From 42be754767a257b7cce2a532aeb12a37720cb20b Mon Sep 17 00:00:00 2001 From: Mikhail Petrov Date: Wed, 14 Aug 2024 12:34:11 +0300 Subject: [PATCH 1/3] web: Add Content-Type display for load page Signed-off-by: Mikhail Petrov --- src/Load.tsx | 1 + src/api.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Load.tsx b/src/Load.tsx index 2cbe1c7..a551203 100644 --- a/src/Load.tsx +++ b/src/Load.tsx @@ -98,6 +98,7 @@ const Load = ({ > File data {`Filename: ${objectData.filename ? objectData.filename : '-'}`} + {`Content-Type: ${objectData.contentType ? objectData.contentType : '-'}`} {`Size: ${objectData.size ? objectData.size : '-'}`} {`Expiration epoch: ${objectData.expirationEpoch ? objectData.expirationEpoch : '-'}`} {`Owner ID: ${objectData.ownerId ? objectData.ownerId : '-'}`} diff --git a/src/api.ts b/src/api.ts index db9be35..a2a6d9f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -7,6 +7,7 @@ export interface ObjectData { containerId?: string | null ownerId?: string | null filename?: string | null + contentType?: string | null size?: string | null expirationEpoch?: string | null } @@ -46,7 +47,7 @@ export default function api(method: Methods, url: string, params: object = {}, h reject(res); } else if (method === 'HEAD' && response.headers) { const res: ObjectData = { - 'filename': response.headers.get('X-Attribute-Filename'), + 'contentType': response.headers.get('Content-Type'), 'containerId': response.headers.get('X-Container-Id'), 'ownerId': response.headers.get('X-Owner-Id'), 'size': response.headers.get('Content-Length') ? response.headers.get('Content-Length') : response.headers.get('x-neofs-payload-length'), From bd17d9a284099b8450d6ed8a89ebdb96321fbeae Mon Sep 17 00:00:00 2001 From: Mikhail Petrov Date: Wed, 14 Aug 2024 12:34:34 +0300 Subject: [PATCH 2/3] readme: Update nginx config Signed-off-by: Mikhail Petrov --- README.md | 65 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 214254b..ab066e2 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ password: proxy_cache_path /srv/neofs_cache/ levels=1:2 keys_zone=neofs_cache:50m max_size=16g inactive=60m use_temp_path=off; server { - set $cid HPUKdZBBtD75jDN8iVb3zoaNACWinuf1vF5kkYpMMbap; - set $data_cid 41tVWBvQVTLGQPHBmXySHsJkcc6X17i39bMuJe9wYhAJ; - set $neofs_http_gateway http.fs.neo.org; + set $cid 7CpJVtBdNvPjjdYwV7q9CghGVPagVLTs71BPQuGQLKSQ; + set $data_cid 754iyTDY8xUtZJZfheSYLUn7jvCkxr79RcbjMt81QykC; + set $neofs_rest_gateway https://rest.fs.neo.org; set $rpc rpc.morph.fs.neo.org:40341; client_max_body_size 100m; proxy_connect_timeout 5m; @@ -79,68 +79,83 @@ server { proxy_read_timeout 5m; send_timeout 5m; default_type application/octet-stream; - + location ~ "^\/chain" { rewrite ^/chain/(.*) /$1 break; proxy_pass https://rpc; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_403 http_429 non_idempotent; + } + + location /gate/objects/ { + rewrite ^/gate/(.*) /v1/objects/$data_cid break; + proxy_pass $neofs_rest_gateway; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_403 http_429; + proxy_pass_request_headers on; } - + location /gate/upload/ { - rewrite ^/gate/(.*) /upload/$data_cid break; - proxy_pass https://$neofs_http_gateway; + rewrite ^/gate/(.*) /v1/upload/$data_cid break; + proxy_pass $neofs_rest_gateway; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_403 http_429; + proxy_pass_request_headers on; proxy_set_header X-Attribute-email $http_x_attribute_email; proxy_set_header X-Attribute-NEOFS-Expiration-Epoch $http_x_attribute_neofs_expiration_epoch; } - + location ~ "^\/gate\/get(/.*)?\/?$" { - rewrite ^/gate/get/(.*) /$data_cid/$1 break; - proxy_pass https://$neofs_http_gateway; + rewrite ^/gate/get/(.*) /v1/get/$data_cid/$1 break; + proxy_pass $neofs_rest_gateway; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_403 http_429; + proxy_intercept_errors on; proxy_cache_valid 404 0; proxy_cache_valid 200 15m; + proxy_buffering on; proxy_cache neofs_cache; proxy_cache_methods GET; } - + location /signup_google/ { proxy_pass http://localhost:8084/login?service=google; proxy_intercept_errors on; + proxy_buffering on; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - + location /signup_github/ { proxy_pass http://localhost:8084/login?service=github; proxy_intercept_errors on; + proxy_buffering on; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - + location ~ "^\/callback" { rewrite ^/callback\?(.*) /$1 break; proxy_pass http://localhost:8084; } - + location /load { - rewrite '^(/.*)$' /get_by_attribute/$cid/FileName/index.html break; - proxy_pass https://$neofs_http_gateway; + rewrite '^(/.*)$' /v1/objects/$cid/by_attribute/FileName/index.html break; + proxy_pass $neofs_rest_gateway; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_403 http_429; + include /etc/nginx/mime.types; } - + location /toc { - rewrite '^(/.*)$' /get_by_attribute/$cid/FileName/index.html break; - proxy_pass https://$neofs_http_gateway; + rewrite '^(/.*)$' /v1/objects/$cid/by_attribute/FileName/index.html break; + proxy_pass $neofs_rest_gateway; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_403 http_429; + include /etc/nginx/mime.types; } - + location / { - rewrite '^(/[0-9a-zA-Z\-]{43,44})$' /get/$cid/$1 break; - rewrite '^/$' /get_by_attribute/$cid/FileName/index.html break; - rewrite '^/([^/]*)$' /get_by_attribute/$cid/FileName/$1 break; - rewrite '^(/.*)$' /get_by_attribute/$cid/FilePath/$1 break; - proxy_pass https://$neofs_http_gateway; + proxy_pass https://$neofs_rest_gateway; } } ``` From bdeee7931586fd32fa0bda77c3908ee52cec4188 Mon Sep 17 00:00:00 2001 From: Mikhail Petrov Date: Wed, 14 Aug 2024 12:35:33 +0300 Subject: [PATCH 3/3] web: Switch to new REST object APIs, closes #114 Signed-off-by: Mikhail Petrov --- .env | 1 - README.md | 2 -- src/App.tsx | 4 ---- src/Home.tsx | 68 ++++++++++++++-------------------------------------- src/api.ts | 7 +++--- 5 files changed, 22 insertions(+), 60 deletions(-) diff --git a/.env b/.env index a57ebe2..177e996 100644 --- a/.env +++ b/.env @@ -1,2 +1 @@ REACT_APP_NEOFS="" -REACT_APP_NETMAP_CONTRACT="0xc4576ea5c3081dd765a17aaaa73d9352e74bdc28" diff --git a/README.md b/README.md index ab066e2..7e72cd6 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,6 @@ Send.NeoFS is a simple example of integration with NeoFS network via HTTP protoc Set variables in the `.env` file before executing the commands: - `REACT_APP_NEOFS` - Path to SendFS -- `REACT_APP_CONTAINER_ID` - NeoFS container ID where the objects would be stored -- `REACT_APP_NETMAP_CONTRACT` - NeoFS netmap contract # Send.NeoFS local up diff --git a/src/App.tsx b/src/App.tsx index 21d537a..244ddc8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,8 +43,6 @@ library.add( interface Environment { version: string | undefined server: string | undefined - netmapContract: string | undefined - epochLine: string | undefined } interface User { @@ -76,8 +74,6 @@ export const App = () => { const [environment] = useState({ version: process.env.REACT_APP_VERSION, server: process.env.REACT_APP_NEOFS, - netmapContract: process.env.REACT_APP_NETMAP_CONTRACT, - epochLine: "c25hcHNob3RFcG9jaA==", }); const [user] = useState(getCookie('X-Bearer') && getCookie('X-Attribute-Email') ? { XBearer: getCookie('X-Bearer'), diff --git a/src/Home.tsx b/src/Home.tsx index fead792..2354c59 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -15,16 +15,6 @@ import { Button, } from 'react-bulma-components'; -function base64ToArrayBuffer(base64: string) { - const bytes: any = new Uint8Array([0, 0, 0, 0]); - const binary_string: string = base64; - const len: number = binary_string.length; - for (let i: number = 0; i < len; i += 1) { - bytes[i] = binary_string.charCodeAt(i); - } - return bytes; -} - const Home = ({ onModal, onScroll, @@ -36,7 +26,7 @@ const Home = ({ }) => { const [files, setFiles] = useState([]); const [dragActive, setDragActive] = useState(false); - const [lifetimeData, setLifetimeData] = useState(12); + const [lifetimeData, setLifetimeData] = useState('12h'); const [isLoading, setLoading] = useState(false); const [isCopied, setCopy] = useState(false); const fileUploadMbLimit: number = 200 * 1024 * 1024; @@ -106,48 +96,26 @@ const Home = ({ } setLoading(true); - api('POST', `/chain`, { - "id":"1", - "jsonrpc":"2.0", - "method":"getstorage", - "params":[ - environment.netmapContract, - environment.epochLine, - ], - }).then((res: any) => { - if (res['result']) { - const epoch_b64: any = atob(res['result']); - const epoch_bytes: any = base64ToArrayBuffer(epoch_b64); - const dv2: any = new DataView(epoch_bytes.buffer); - const epoch: string | number = dv2.getUint32(0, true); - - files.forEach((file: File) => { - const formData: any = new FormData(); - formData.append('file', file); - onUploadFile(formData, file.name, epoch, lifetimeData); - }); - } else { - setLoading(false); - onModal('failed', 'Something went wrong, try again'); - } - }).catch(() => { - setLoading(false); - onModal('failed', 'Something went wrong, try again'); + files.forEach((file: File) => { + onUploadFile(file); }); e.preventDefault(); }; - const onUploadFile = (formData: any, filename: string | null, epoch: string | number, lifetime: string | number) => { - document.cookie = `Bearer=${user.XBearer}; path=/gate/upload; expires=${new Date(Date.now() + 10 * 1000).toUTCString()}`; - api('POST', '/gate/upload/', formData, { - 'X-Attribute-NEOFS-Expiration-Epoch': String(Number(epoch) + Number(lifetime)), - 'X-Attribute-email': user.XAttributeEmail, - 'Content-Type': 'multipart/form-data', + const onUploadFile = (file: any | null) => { + document.cookie = `Bearer=${user.XBearer}; path=/gate/objects; expires=${new Date(Date.now() + 10 * 1000).toUTCString()}`; + api('POST', "/gate/objects/", file, { + 'X-Attributes': JSON.stringify({ + 'FileName': file.name, + 'Email': user.XAttributeEmail, + }), + 'X-Neofs-Expiration-Duration': lifetimeData, + 'Content-Type': file.type, }).then((res: any) => { - res['filename'] = filename; + res['filename'] = file.name; setUploadedObjects((uploadedObjectsTemp: UploadedObject[]) => [...uploadedObjectsTemp, res]); setFiles((files: File[]) => { - const filesTemp = files.filter((item: File) => item.name !== filename); + const filesTemp = files.filter((item: File) => item.name !== file.name); if (filesTemp.length === 0) { setLoading(false); } @@ -211,10 +179,10 @@ const Home = ({ value={lifetimeData} disabled={isLoading} > - - - - + + + + diff --git a/src/api.ts b/src/api.ts index a2a6d9f..b1f71d0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -18,9 +18,8 @@ async function serverRequest(method: Methods, url: string, params: object, heade headers, } - if (json['headers']['Content-Type'] === 'multipart/form-data') { + if (json['headers']['Content-Type']) { json['body'] = params; - delete json['headers']['Content-Type']; } else if (Object.keys(params).length > 0) { json['body'] = JSON.stringify(params); json['headers']['Content-Type'] = 'application/json'; @@ -46,12 +45,14 @@ export default function api(method: Methods, url: string, params: object = {}, h if (method === 'HEAD' && response.status !== 200) { reject(res); } else if (method === 'HEAD' && response.headers) { + const attributes: any = response.headers.get('X-Attributes') ? JSON.parse(response.headers.get('X-Attributes')) : {}; const res: ObjectData = { + 'filename': attributes['FileName'], 'contentType': response.headers.get('Content-Type'), 'containerId': response.headers.get('X-Container-Id'), 'ownerId': response.headers.get('X-Owner-Id'), 'size': response.headers.get('Content-Length') ? response.headers.get('Content-Length') : response.headers.get('x-neofs-payload-length'), - 'expirationEpoch': response.headers.get('X-Attribute-Neofs-Expiration-Epoch'), + 'expirationEpoch': attributes['__NEOFS__EXPIRATION_EPOCH'], } resolve(res); } else if (method === 'GET' && url.indexOf(`/gate/get/`) !== -1) {