From d08a02b6e7637304057d5a775e19a99b440fd20a Mon Sep 17 00:00:00 2001 From: user Date: Fri, 26 Sep 2025 12:21:27 +0300 Subject: [PATCH] new version with admin --- package-lock.json | 184 ++- package.json | 1 + src/app/router/constants/index.ts | 1 + src/app/router/index.tsx | 6 +- src/pages/admin/index.tsx | 1085 +++++++++++++++++ src/pages/root/steps/presubmit-step/index.tsx | 26 +- src/shared/libs/request/index.ts | 101 +- src/shared/services/admin/index.ts | 421 +++++++ src/shared/services/file/index.ts | 563 ++++++--- yarn.lock | 155 ++- 10 files changed, 2357 insertions(+), 186 deletions(-) create mode 100644 src/pages/admin/index.tsx create mode 100644 src/shared/services/admin/index.ts diff --git a/package-lock.json b/package-lock.json index 12fba59..17317ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "axios": "^1.6.7", "buffer": "^6.0.3", "clsx": "^2.1.0", - "jssha": "^3.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.51.0", @@ -25,6 +24,7 @@ "react-router-dom": "^6.22.2", "react-tag-input": "^6.10.3", "tailwind-merge": "^2.2.1", + "tus-js-client": "^4.3.1", "zod": "^3.22.4", "zustand": "^4.5.2" }, @@ -2298,6 +2298,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2434,6 +2440,15 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combine-errors": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/combine-errors/-/combine-errors-3.0.3.tgz", + "integrity": "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==", + "dependencies": { + "custom-error-instance": "2.1.1", + "lodash.uniqby": "4.5.0" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2496,6 +2511,12 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/custom-error-instance": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz", + "integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==", + "license": "ISC" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3683,6 +3704,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4062,6 +4089,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -4158,6 +4197,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -4222,15 +4267,6 @@ "node": ">=6" } }, - "node_modules/jssha": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", - "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4294,12 +4330,74 @@ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "license": "MIT" }, + "node_modules/lodash._baseiteratee": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz", + "integrity": "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==", + "license": "MIT", + "dependencies": { + "lodash._stringtopath": "~4.8.0" + } + }, + "node_modules/lodash._basetostring": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz", + "integrity": "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==", + "license": "MIT" + }, + "node_modules/lodash._baseuniq": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz", + "integrity": "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==", + "license": "MIT", + "dependencies": { + "lodash._createset": "~4.0.0", + "lodash._root": "~3.0.0" + } + }, + "node_modules/lodash._createset": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz", + "integrity": "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==", + "license": "MIT" + }, + "node_modules/lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==", + "license": "MIT" + }, + "node_modules/lodash._stringtopath": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz", + "integrity": "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==", + "license": "MIT", + "dependencies": { + "lodash._basetostring": "~4.12.0" + } + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz", + "integrity": "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==", + "license": "MIT", + "dependencies": { + "lodash._baseiteratee": "~4.7.0", + "lodash._baseuniq": "~4.6.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5053,6 +5151,23 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5067,6 +5182,12 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5341,6 +5462,12 @@ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5367,6 +5494,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6040,6 +6176,24 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/tus-js-client": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-4.3.1.tgz", + "integrity": "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.1.2", + "combine-errors": "^3.0.3", + "is-stream": "^2.0.0", + "js-base64": "^3.7.2", + "lodash.throttle": "^4.1.1", + "proper-lockfile": "^4.1.2", + "url-parse": "^1.5.7" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -6257,6 +6411,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 8559d60..d4f55d7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-router-dom": "^6.22.2", "react-tag-input": "^6.10.3", "tailwind-merge": "^2.2.1", + "tus-js-client": "^4.3.1", "zod": "^3.22.4", "zustand": "^4.5.2" }, diff --git a/src/app/router/constants/index.ts b/src/app/router/constants/index.ts index 09322d5..0c22278 100644 --- a/src/app/router/constants/index.ts +++ b/src/app/router/constants/index.ts @@ -2,4 +2,5 @@ export const Routes = { Root: "/uploadContent", ViewContent: "/viewContent", SentryCheck: "/sentryCheck", + Admin: "/admin", }; diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index b70c13e..f0cbc5c 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -3,6 +3,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { Routes } from "~/app/router/constants"; import { RootPage } from "~/pages/root"; import { ViewContentPage } from "~/pages/view-content"; +import { AdminPage } from "~/pages/admin"; import { ProtectedLayout } from "./protected-layout"; const router = createBrowserRouter([ @@ -11,8 +12,8 @@ const router = createBrowserRouter([ children: [ { path: Routes.Root, element: }, { path: Routes.ViewContent, element: }, - { - path: Routes.SentryCheck, + { + path: Routes.SentryCheck, element: (
+ + Cookie max-age {Math.round(172800 / 3600)} ч + +
+ + + ); + }; + + const renderOverview = () => { + if (!overviewQuery.data) { + return overviewQuery.isLoading ?

Загрузка обзора…

:

Нет данных.

; + } + const { project, services, ton, runtime, ipfs } = overviewQuery.data; + return ( +
overviewQuery.refetch()} + > + Обновить + + } + > +
+ {overviewCards.map((card) => ( +
+

{card.label}

+

{card.value}

+ {card.helper ?

{card.helper}

: null} +
+ ))} +
+ +
+
+

Проект

+
+ {project.host || "—"} + {project.name} + {project.privacy} + {ton.testnet ? "Да" : "Нет"} + {ton.api_key_configured ? "Настроен" : "Нет"} + {ton.host || "—"} +
+
+
+

Среда выполнения

+
+ {runtime.python} + {runtime.implementation} + {runtime.platform} + {formatDate(runtime.utc_now)} +
+
+
+ +
+
+

IPFS

+
+ {(ipfs.identity as Record)?.ID ?? "—"} + {(ipfs.identity as Record)?.AgentVersion ?? "—"} + {numberFormatter.format(Number((ipfs.bitswap as Record)?.Peers ?? 0))} + {formatBytes(Number((ipfs.repo as Record)?.RepoSize ?? 0))} + {formatBytes(Number((ipfs.repo as Record)?.StorageMax ?? 0))} +
+
+
+

Сервисы

+
    + {services.length === 0 ? ( +
  • Нет зарегистрированных сервисов
  • + ) : ( + services.map((service) => { + const status = service.status ?? "—"; + const tone: "success" | "warn" | "danger" | "neutral" = status.includes("working") + ? "success" + : status.includes("timeout") + ? "danger" + : "neutral"; + return ( +
  • + {service.name} +
    + + {service.last_reported_seconds != null + ? `Обновлён ${Math.round(service.last_reported_seconds)} сек назад` + : "Нет данных"} + + {status} +
    +
  • + ); + }) + )} +
+
+
+
+ ); + }; + + const renderStorage = () => { + if (storageQuery.isLoading) { + return
Загрузка…
; + } + if (!storageQuery.data) { + return null; + } + const { directories, disk, derivatives } = storageQuery.data; + return ( +
+
+ + + + + + + + + + + {directories.map((dir) => ( + + + + + + + ))} + +
ПутьФайловРазмерСостояние
{dir.path}{numberFormatter.format(dir.file_count)}{formatBytes(dir.size_bytes)} + {dir.exists ? "OK" : "Нет"} +
+
+ {disk ? ( +
+

Снимок диска

+
+ {disk.path} + {formatBytes(disk.free_bytes)} + {formatBytes(disk.used_bytes)} + {formatBytes(disk.total_bytes)} + {disk.percent_used != null ? `${disk.percent_used}%` : "—"} +
+
+ ) : null} +
+

Деривативы

+
+ {numberFormatter.format(derivatives.ready)} + {numberFormatter.format(derivatives.processing)} + {numberFormatter.format(derivatives.pending)} + {numberFormatter.format(derivatives.failed)} + {formatBytes(derivatives.total_bytes)} +
+
+
+ ); + }; + + const renderUploads = () => { + if (!uploadsQuery.data) { + return uploadsQuery.isLoading ? ( +
Загрузка…
+ ) : null; + } + const { states, total, recent } = uploadsQuery.data; + return ( +
uploadsQuery.refetch()} + > + Обновить + + } + > +
+ + {Object.entries(states).map(([state, count]) => ( + + ))} +
+
+ + + + + + + + + + + + {recent.map((item) => ( + + + + + + + + ))} + +
IDФайлРазмерСтатусОбновлено
{item.id}{item.filename || "—"}{item.size_bytes ? formatBytes(item.size_bytes) : "—"}{item.state}{formatDate(item.updated_at)}
+
+
+ ); + }; + + const renderSystem = () => { + if (!systemQuery.data) { + return systemQuery.isLoading ? ( +
Загрузка…
+ ) : null; + } + const { env, service_config, services, blockchain_tasks, latest_index_items } = systemQuery.data; + return ( +
+
+
+

Окружение

+
+ {Object.entries(env).map(([key, value]) => ( + {value ?? "—"} + ))} +
+
+
+

Задачи блокчейна

+
+ {Object.entries(blockchain_tasks).map(([key, value]) => ( + {numberFormatter.format(value)} + ))} +
+

Последние индексы

+
    + {latest_index_items.length === 0 ? ( +
  • Список пуст
  • + ) : ( + latest_index_items.map((item) => ( +
  • +
    {item.encrypted_cid ?? "—"}
    +
    {formatDate(item.updated_at)}
    +
  • + )) + )} +
+
+
+
+

ServiceConfig

+
+ + + + + + + + + + {service_config.map((item) => ( + + + + + + ))} + +
КлючЗначениеRAW
{item.key}{item.value ?? "—"}{item.raw ?? "—"}
+
+
+
+

Сервисы

+
    + {services.map((service) => ( +
  • + {service.name} + {service.status ?? "—"} +
  • + ))} +
+
+
+ ); + }; + + const renderBlockchain = () => { + if (!blockchainQuery.data) { + return blockchainQuery.isLoading ? ( +
Загрузка…
+ ) : null; + } + const { counts, recent } = blockchainQuery.data; + return ( +
blockchainQuery.refetch()} + > + Обновить + + } + > +
+ {Object.entries(counts).map(([key, value]) => ( + + ))} +
+
+ + + + + + + + + + + + + + {recent.map((task) => ( + + + + + + + + + + ))} + +
IDНазначениеСуммаСтатусEpoch · SeqnoHashОбновлено
{task.id}{task.destination || "—"}{task.amount || "—"}{task.status}{task.epoch ?? "—"} · {task.seqno ?? "—"}{task.transaction_hash || "—"}{formatDate(task.updated)}
+
+
+ ); + }; + + const renderNodes = () => { + if (!nodesQuery.data) { + return nodesQuery.isLoading ? ( +
Загрузка…
+ ) : null; + } + const { items } = nodesQuery.data; + const handleRoleChange = (nodePublicKey: string | null, nodeIp: string | null) => { + return (event: ChangeEvent) => { + const role = event.target.value as "trusted" | "read-only" | "deny"; + nodeRoleMutation.mutate({ + role, + public_key: nodePublicKey ?? undefined, + host: nodeIp ?? undefined, + }); + }; + }; + return ( +
+
+ + + + + + + + + + + + + + {items.map((node, index) => ( + + + + + + + + + + ))} + +
IPПортПубличный ключРольВерсияПоследний онлайнЗаметки
{node.ip || "—"}{node.port ?? "—"}{node.public_key || "—"} + + {node.version || "—"}{formatDate(node.last_seen)}{node.notes || "—"}
+
+
+ ); + }; + + const renderStatus = () => { + if (!statusQuery.data) { + return statusQuery.isLoading ? ( +
Загрузка…
+ ) : null; + } + const { ipfs, pin_counts, derivatives, convert_backlog, limits } = statusQuery.data; + return ( +
+
+
+
+

IPFS

+
+ {formatBytes(Number((ipfs.repo as Record)?.RepoSize ?? 0))} + {formatBytes(Number((ipfs.repo as Record)?.StorageMax ?? 0))} + {numberFormatter.format(Number((ipfs.bitswap as Record)?.Peers ?? 0))} + {numberFormatter.format(Number((ipfs.bitswap as Record)?.BlocksReceived ?? 0))} +
+
+
+

Pin-статистика

+
+ {Object.entries(pin_counts).map(([key, value]) => ( + {numberFormatter.format(value)} + ))} + {numberFormatter.format(convert_backlog)} + {formatBytes(derivatives.total_bytes)} +
+
+
+
+
{ + cacheLimitsMutation.mutate(values); + })} + > +

Лимиты кэша

+
+ + +
+ +
+ +
{ + cacheCleanupMutation.mutate({ mode: "fit", max_gb: values.max_gb }); + })} + > +

Очистка по размеру

+ +
+ + +
+
+ +
{ + syncLimitsMutation.mutate(values); + })} + > +

Sync лимиты

+
+ + +
+ +
+
+
+
+ ); + }; + + const renderDashboard = () => { + if (authState === "checking" && overviewQuery.isLoading) { + return ( +
Проверяем сессию…
+ ); + } + return ( +
+
+
+

Админ-панель

+

+ Управление узлом, мониторинг IPFS/TON и контроль кэша +

+
+
+ + +
+
+ + + + {renderOverview()} + {renderStorage()} + {renderUploads()} + {renderSystem()} + {renderBlockchain()} + {renderNodes()} + {renderStatus()} +
+ ); + }; + + return ( +
+
+ {renderFlash()} + {authState === "authorized" || authState === "checking" ? renderDashboard() : renderLoginPanel()} +
+
+ ); +}; + +const InfoRow = ({ label, children }: { label: string; children: ReactNode }) => { + return ( +
+
{label}
+
{children}
+
+ ); +}; + +const MetricCard = ({ label, value }: { label: string; value: string }) => { + return ( +
+

{label}

+

{value}

+
+ ); +}; diff --git a/src/pages/root/steps/presubmit-step/index.tsx b/src/pages/root/steps/presubmit-step/index.tsx index 5e66324..9e91ef6 100644 --- a/src/pages/root/steps/presubmit-step/index.tsx +++ b/src/pages/root/steps/presubmit-step/index.tsx @@ -8,7 +8,7 @@ import ReactPlayer from "react-player/lazy"; import { Button } from "~/shared/ui/button"; import { useRootStore } from "~/shared/stores/root"; import { FormLabel } from "~/shared/ui/form-label"; -import { useUploadFile } from "~/shared/services/file"; +import { useLegacyUploadFile, useTusUpload } from "~/shared/services/file"; import { Progress } from "~/shared/ui/progress"; import { useCreateNewContent } from "~/shared/services/content"; import { BackButton } from "~/shared/ui/back-button"; @@ -31,8 +31,8 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => { const [isCoverExpanded, setCoverExpanded] = useState(false); - const uploadCover = useUploadFile(); - const uploadFile = useUploadFile(); + const uploadCover = useLegacyUploadFile(); + const uploadFile = useTusUpload(); const createContent = useCreateNewContent(); @@ -54,9 +54,21 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => { try { let coverUploadResult = { content_id_v1: "" }; - const fileUploadResult = await uploadFile.mutateAsync( - rootStore.file as File, - ); + const fileUploadResult = await uploadFile.mutateAsync({ + file: rootStore.file as File, + metadata: { + title: rootStore.name, + description: "", + content_type: rootStore.file?.type || "application/octet-stream", + preview_start_ms: 0, + preview_duration_ms: 30000, + downloadable: rootStore.allowDwnld ? "1" : "0", + }, + }); + + if (!fileUploadResult.encryptedCid) { + throw new Error("Tus upload did not return encrypted cid"); + } if (rootStore.allowCover && rootStore.cover) { coverUploadResult = await uploadCover.mutateAsync( @@ -76,7 +88,7 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => { // следует отметить что вы должны еще откомментровать AuthorsStep в RootPage // authors: rootStore.authors, downloadable: rootStore.allowDwnld, - content: fileUploadResult.content_id_v1, + content: fileUploadResult.encryptedCid, image: coverUploadResult.content_id_v1, price: String(rootStore.price * 10 ** 9), hashtags: rootStore.hashtags, diff --git a/src/shared/libs/request/index.ts b/src/shared/libs/request/index.ts index 9eae009..25df920 100644 --- a/src/shared/libs/request/index.ts +++ b/src/shared/libs/request/index.ts @@ -1,17 +1,102 @@ -import axios from 'axios'; +import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; -export const APP_API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +import { getAdminAuthSnapshot } from '~/shared/libs/admin-auth'; -export const request = axios.create({ - baseURL: APP_API_BASE_URL, +const API_BASE_PATH = '/api/v1'; +const API_ENDPOINTS = [ + 'https://my-public-node-8.projscale.dev', + 'http://localhost:3000', +] as const; + +const API_BASE_URLS = API_ENDPOINTS.map((endpoint) => { + return `${endpoint.replace(/\/$/, '')}${API_BASE_PATH}`; }); -request.interceptors.request.use((config) => { - const auth_v1_token = localStorage.getItem('auth_v1_token'); +const ENDPOINT_INDEX_KEY = '__apiEndpointIndex__'; - if (auth_v1_token) { - config.headers.Authorization = auth_v1_token; +const isBrowser = typeof window !== 'undefined'; + +const readStorage = (key: string) => { + if (!isBrowser) { + return null; } + try { + return window.localStorage.getItem(key); + } catch (error) { + console.error('Failed to access localStorage', error); + return null; + } +}; + +type RequestConfig = InternalAxiosRequestConfig & { + [ENDPOINT_INDEX_KEY]?: number; +}; + +export const request = axios.create({ withCredentials: true }); + +request.interceptors.request.use((config: RequestConfig) => { + config.headers = config.headers ?? {}; + const headers = config.headers as Record; + const authToken = readStorage('auth_v1_token'); + + if (authToken && !headers.Authorization) { + headers.Authorization = authToken; + } + + const urlPath = config.url ?? ''; + if (urlPath.startsWith('/admin')) { + const { token, headerName } = getAdminAuthSnapshot(); + if (token) { + const headerKey = headerName || 'X-Admin-Token'; + if (!headers[headerKey]) { + headers[headerKey] = token; + } + } + } + + const hasCustomBaseUrl = Boolean(config.baseURL && !API_BASE_URLS.includes(config.baseURL)); + if (hasCustomBaseUrl) { + return config; + } + + const attempt = typeof config[ENDPOINT_INDEX_KEY] === 'number' ? (config[ENDPOINT_INDEX_KEY] as number) : 0; + config[ENDPOINT_INDEX_KEY] = attempt; + config.baseURL = API_BASE_URLS[attempt] ?? API_BASE_URLS[0]; return config; }); + +request.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const config = error.config as RequestConfig | undefined; + + if (!config) { + return Promise.reject(error); + } + + const hasCustomBaseUrl = Boolean(config.baseURL && !API_BASE_URLS.includes(config.baseURL)); + if (hasCustomBaseUrl) { + return Promise.reject(error); + } + + const isAbortError = error.code === 'ERR_CANCELED' || error.message === 'canceled'; + const isNetworkError = !error.response && !isAbortError; + + if (!isNetworkError) { + return Promise.reject(error); + } + + const currentIndex = typeof config[ENDPOINT_INDEX_KEY] === 'number' ? (config[ENDPOINT_INDEX_KEY] as number) : 0; + const nextIndex = currentIndex + 1; + + if (nextIndex >= API_BASE_URLS.length) { + return Promise.reject(error); + } + + config[ENDPOINT_INDEX_KEY] = nextIndex; + config.baseURL = API_BASE_URLS[nextIndex]; + + return request(config); + }, +); diff --git a/src/shared/services/admin/index.ts b/src/shared/services/admin/index.ts new file mode 100644 index 0000000..40080e5 --- /dev/null +++ b/src/shared/services/admin/index.ts @@ -0,0 +1,421 @@ +import axios, { AxiosError } from 'axios'; +import { + useMutation, + useQuery, + UseMutationOptions, + UseQueryOptions, +} from 'react-query'; + +import { request } from "~/shared/libs"; +import { clearAdminAuth, persistAdminAuth } from "~/shared/libs/admin-auth"; + +export type AdminServiceState = { + name: string; + status?: string; + last_reported_seconds: number | null; +}; + +export type AdminOverviewResponse = { + project: { + host: string | null; + name: string; + privacy: string; + }; + codebase: { + branch: string | null; + commit: string | null; + }; + node: { + id: string; + service_wallet: string; + ton_master: string; + }; + runtime: { + python: string; + implementation: string; + platform: string; + utc_now: string; + }; + ipfs: Record & { + identity?: Record | { error: string }; + bitswap?: Record | { error: string }; + repo?: Record | { error: string }; + }; + content: { + encrypted_total: number; + upload_sessions_total: number; + derivatives_ready: number; + }; + ton: { + host: string | null; + api_key_configured: boolean; + testnet: boolean; + }; + services: AdminServiceState[]; +}; + +export type AdminStorageResponse = { + directories: Array<{ + label: string; + path: string; + exists: boolean; + file_count: number; + size_bytes: number; + }>; + disk: null | { + path: string; + total_bytes: number; + used_bytes: number; + free_bytes: number; + percent_used: number | null; + }; + derivatives: { + ready: number; + processing: number; + pending: number; + failed: number; + total_bytes: number; + }; +}; + +export type AdminUploadsResponse = { + total: number; + states: Record; + recent: Array<{ + id: string; + filename: string | null; + size_bytes: number | null; + state: string; + encrypted_cid: string | null; + error: string | null; + updated_at: string; + created_at: string; + }>; +}; + +export type AdminSystemResponse = { + env: Record; + service_config: Array<{ + key: string; + value: string | null; + raw: string | null; + }>; + services: AdminServiceState[]; + blockchain_tasks: Record; + latest_index_items: Array<{ + encrypted_cid: string | null; + updated_at: string; + }>; +}; + +export type AdminBlockchainResponse = { + counts: Record; + recent: Array<{ + id: string; + destination: string | null; + amount: string | null; + status: string; + epoch: number | null; + seqno: number | null; + transaction_hash: string | null; + updated: string; + }>; +}; + +export type AdminNodesResponse = { + items: Array<{ + ip: string | null; + port: number | null; + public_key: string | null; + role: 'trusted' | 'read-only' | 'deny'; + version?: string | null; + last_seen?: string | null; + notes?: string | null; + }>; +}; + +export type AdminStatusResponse = { + ipfs: { + bitswap: Record; + repo: Record; + }; + pin_counts: Record; + derivatives: { + ready: number; + processing: number; + pending: number; + failed: number; + total_bytes: number; + }; + convert_backlog: number; + limits: { + DERIVATIVE_CACHE_MAX_GB: number; + DERIVATIVE_CACHE_TTL_DAYS: number; + SYNC_MAX_CONCURRENT_PINS: number; + SYNC_DISK_LOW_WATERMARK_PCT: number; + }; +}; + +export type AdminCacheSetLimitsPayload = { + max_gb: number; + ttl_days: number; +}; + +export type AdminCacheCleanupPayload = + | { + mode: 'ttl'; + } + | { + mode?: 'fit'; + max_gb: number; + }; + +export type AdminSyncSetLimitsPayload = { + max_concurrent_pins: number; + disk_low_watermark_pct: number; +}; + +export type AdminNodeSetRolePayload = { + public_key?: string; + host?: string; + role: 'trusted' | 'read-only' | 'deny'; +}; + +const defaultQueryOptions = { + staleTime: 30_000, + refetchOnWindowFocus: false, +} as const; + +type QueryOptions = UseQueryOptions< + TData, + AxiosError, + TData, + TQueryKey +>; + +type MutationOptions = UseMutationOptions< + TData, + AxiosError, + TVariables +>; + +export const useAdminOverview = ( + options?: QueryOptions, +) => { + return useQuery( + ['admin', 'overview'], + async () => { + const { data } = await request.get('/admin.overview'); + return data; + }, + { + ...defaultQueryOptions, + ...options, + }, + ); +}; + +export const useAdminStorage = ( + options?: QueryOptions, +) => { + return useQuery( + ['admin', 'storage'], + async () => { + const { data } = await request.get('/admin.storage'); + return data; + }, + { + ...defaultQueryOptions, + ...options, + }, + ); +}; + +export const useAdminUploads = ( + options?: QueryOptions, +) => { + return useQuery( + ['admin', 'uploads'], + async () => { + const { data } = await request.get('/admin.uploads'); + return data; + }, + { + ...defaultQueryOptions, + ...options, + }, + ); +}; + +export const useAdminSystem = ( + options?: QueryOptions, +) => { + return useQuery( + ['admin', 'system'], + async () => { + const { data } = await request.get('/admin.system'); + return data; + }, + { + ...defaultQueryOptions, + ...options, + }, + ); +}; + +export const useAdminBlockchain = ( + options?: QueryOptions, +) => { + return useQuery( + ['admin', 'blockchain'], + async () => { + const { data } = await request.get('/admin.blockchain'); + return data; + }, + { + ...defaultQueryOptions, + ...options, + }, + ); +}; + +export const useAdminNodes = ( + options?: QueryOptions, +) => { + return useQuery( + ['admin', 'nodes'], + async () => { + const { data } = await request.get('/admin.nodes'); + return data; + }, + { + ...defaultQueryOptions, + ...options, + }, + ); +}; + +export const useAdminStatus = ( + options?: QueryOptions, +) => { + return useQuery( + ['admin', 'status'], + async () => { + const { data } = await request.get('/admin.status'); + return data; + }, + { + ...defaultQueryOptions, + ...options, + }, + ); +}; + +export type AdminLoginResponse = { + ok: true; + cookie_name?: string; + header_name?: string; + max_age?: number; +}; + +export const useAdminLogin = ( + options?: MutationOptions, +) => { + return useMutation( + async ({ secret }) => { + const { data } = await request.post('/admin.login', { secret }); + persistAdminAuth({ + token: secret, + headerName: data.header_name, + cookieName: data.cookie_name, + maxAge: data.max_age, + }); + return data; + }, + options, + ); +}; + +export const useAdminLogout = ( + options?: MutationOptions<{ ok: true }, void>, +) => { + return useMutation<{ ok: true }, AxiosError, void>( + async () => { + const { data } = await request.post<{ ok: true }>('/admin.logout'); + clearAdminAuth(); + return data; + }, + options, + ); +}; + +export const useAdminCacheSetLimits = ( + options?: MutationOptions<{ ok: true }, AdminCacheSetLimitsPayload>, +) => { + return useMutation<{ ok: true }, AxiosError, AdminCacheSetLimitsPayload>( + async (payload) => { + const { data } = await request.post<{ ok: true }>('/admin.cache.setLimits', payload); + return data; + }, + options, + ); +}; + +export const useAdminCacheCleanup = ( + options?: MutationOptions<{ ok: true; removed?: number }, AdminCacheCleanupPayload>, +) => { + return useMutation<{ ok: true; removed?: number }, AxiosError, AdminCacheCleanupPayload>( + async (payload) => { + const { data } = await request.post<{ ok: true; removed?: number }>('/admin.cache.cleanup', payload); + return data; + }, + options, + ); +}; + +export const useAdminSyncSetLimits = ( + options?: MutationOptions<{ ok: true }, AdminSyncSetLimitsPayload>, +) => { + return useMutation<{ ok: true }, AxiosError, AdminSyncSetLimitsPayload>( + async (payload) => { + const { data } = await request.post<{ ok: true }>('/admin.sync.setLimits', payload); + return data; + }, + options, + ); +}; + +export const useAdminNodeSetRole = ( + options?: MutationOptions<{ ok: true; node: { ip: string | null; public_key: string | null; role: string } }, AdminNodeSetRolePayload>, +) => { + return useMutation< + { ok: true; node: { ip: string | null; public_key: string | null; role: string } }, + AxiosError, + AdminNodeSetRolePayload + >( + async (payload) => { + const { data } = await request.post<{ ok: true; node: { ip: string | null; public_key: string | null; role: string } }>( + '/admin.node.setRole', + payload, + ); + return data; + }, + options, + ); +}; + +export const useAdminSyncLimits = () => { + const cacheSetLimits = useAdminCacheSetLimits(); + const syncSetLimits = useAdminSyncSetLimits(); + return { cacheSetLimits, syncSetLimits }; +}; + +export const isUnauthorizedError = (error: unknown) => { + if (!error) { + return false; + } + if (axios.isAxiosError(error)) { + return error.response?.status === 401; + } + return false; +}; diff --git a/src/shared/services/file/index.ts b/src/shared/services/file/index.ts index 60bd57e..9110ba2 100644 --- a/src/shared/services/file/index.ts +++ b/src/shared/services/file/index.ts @@ -1,201 +1,468 @@ import { useMutation } from "react-query"; -import { useState } from "react"; +import { useRef, useState } from "react"; +import { Upload } from "tus-js-client"; + import { request } from "~/shared/libs"; const STORAGE_API_URL = import.meta.env.VITE_API_BASE_STORAGE_URL; const MAX_CHUNK_SIZE = 80 * 1024 * 1024; // 80 MB +const TUS_ENDPOINT = import.meta.env.VITE_TUS_ENDPOINT?.trim(); -export const useUploadFile = () => { +const TUS_STATUS_POLL_INTERVAL_MS = 2000; +const TUS_STATUS_POLL_TIMEOUT_MS = 5 * 60 * 1000; + +const sleep = (ms: number) => new Promise((resolve) => { + setTimeout(resolve, ms); +}); + +const normalizeError = (error: unknown, fallbackMessage: string) => { + if (error instanceof Error) { + return error; + } + + if (typeof error === "string") { + return new Error(error); + } + + if (error && typeof error === "object" && "message" in error) { + const message = (error as { message?: unknown }).message; + if (typeof message === "string") { + return new Error(message); + } + } + + return new Error(fallbackMessage); +}; + +const sanitizeMetadataKey = (key: string) => { + return key.replace(/[^A-Za-z0-9_.-]/g, "_"); +}; + +const normalizeMetadata = ( + metadata?: Record, +) => { + if (!metadata) { + return {} as Record; + } + + return Object.entries(metadata).reduce((acc, [rawKey, rawValue]) => { + if (rawValue === undefined || rawValue === null) { + return acc; + } + + const key = sanitizeMetadataKey(rawKey); + if (!key) { + return acc; + } + + acc[key] = String(rawValue); + return acc; + }, {} as Record); +}; + +const extractUploadId = (uploadUrl: string) => { + try { + const url = new URL(uploadUrl); + const parts = url.pathname.split("/").filter(Boolean); + const uploadId = parts[parts.length - 1]; + if (!uploadId) { + throw new Error("Empty upload id"); + } + return uploadId; + } catch (error) { + const normalized = uploadUrl.split("/").filter(Boolean); + const uploadId = normalized[normalized.length - 1]; + if (!uploadId) { + throw new Error("Unable to extract upload id from tus Location"); + } + return uploadId; + } +}; + +type UploadSessionState = "uploading" | "processing" | "pinned" | "failed"; + +type UploadStatusResponse = { + id: string; + state: UploadSessionState; + encrypted_cid?: string | null; + size_bytes?: number | null; + error?: string | null; +}; + +const pollUploadStatus = async ( + uploadId: string, + signal?: AbortSignal, +): Promise => { + const startedAt = Date.now(); + + while (true) { + if (signal?.aborted) { + throw new DOMException("Tus status polling aborted", "AbortError"); + } + + try { + const { data } = await request.get( + `/upload.status/${uploadId}`, + { + headers: { "Cache-Control": "no-store" }, + }, + ); + + if (data.state === "failed") { + throw new Error(data.error || "Tus upload failed on server"); + } + + if (data.state === "pinned" && data.encrypted_cid) { + return data; + } + } catch (error) { + const maybeAxios = error as { response?: { status?: number } }; + if (maybeAxios?.response?.status === 404) { + // Hook has not recorded the upload session yet; continue polling. + } else if (error instanceof DOMException && error.name === "AbortError") { + throw error; + } else { + throw normalizeError(error, "Tus status polling failed"); + } + } + + if (Date.now() - startedAt > TUS_STATUS_POLL_TIMEOUT_MS) { + throw new Error("Timed out waiting for tus upload finalization"); + } + + await sleep(TUS_STATUS_POLL_INTERVAL_MS); + } +}; + +type TusUploadArgs = { + file: File; + metadata?: Record; + signal?: AbortSignal; +}; + +type TusUploadResult = { + kind: "tus"; + uploadId: string; + encryptedCid: string; + sizeBytes?: number; + state: UploadSessionState; +}; + +export const useTusUpload = () => { + const [uploadProgress, setUploadProgress] = useState(0); + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + const activeUploadRef = useRef(null); + + const mutation = useMutation( + ["upload-file", "tus"], + async ({ file, metadata, signal }) => { + if (!file) { + throw new Error("File is required for tus upload"); + } + + if (!TUS_ENDPOINT) { + throw new Error("Tus endpoint is not configured"); + } + + setIsUploading(true); + setUploadProgress(0); + setUploadError(null); + + try { + const result = await new Promise((resolve, reject) => { + const token = localStorage.getItem("auth_v1_token") ?? ""; + const normalizedMeta = normalizeMetadata(metadata); + const headers: Record = { "Cache-Control": "no-store" }; + if (token) { + headers.Authorization = token; + } + + const upload = new Upload(file, { + endpoint: TUS_ENDPOINT, + metadata: { + filename: file.name, + content_type: file.type || "application/octet-stream", + ...normalizedMeta, + }, + headers, + retryDelays: [0, 1000, 3000, 5000], + removeFingerprintOnSuccess: true, + uploadDataDuringCreation: true, + onError: (err) => { + const normalized = normalizeError(err, "Tus upload failed"); + setUploadError(normalized); + setIsUploading(false); + setUploadProgress(0); + activeUploadRef.current = null; + reject(normalized); + }, + onProgress: (bytesUploaded, bytesTotal) => { + const percent = bytesTotal + ? Math.floor((bytesUploaded / bytesTotal) * 100) + : 0; + setUploadProgress(Math.min(99, Math.max(0, percent))); + }, + onSuccess: () => { + (async () => { + try { + setUploadProgress(99); + const uploadUrl = upload.url; + if (!uploadUrl) { + throw new Error("Tus upload finished without Location header"); + } + + const uploadId = extractUploadId(uploadUrl); + const status = await pollUploadStatus(uploadId, signal); + const encryptedCid = status.encrypted_cid; + if (!encryptedCid) { + throw new Error("Tus upload finalized without encrypted CID"); + } + + setUploadProgress(100); + setIsUploading(false); + activeUploadRef.current = null; + + resolve({ + kind: "tus", + uploadId, + encryptedCid, + sizeBytes: status.size_bytes ?? undefined, + state: status.state, + }); + } catch (statusError) { + const normalized = normalizeError( + statusError, + "Failed to finalize tus upload", + ); + setUploadError(normalized); + setIsUploading(false); + activeUploadRef.current = null; + reject(normalized); + } + })(); + }, + }); + + activeUploadRef.current = upload; + + if (signal) { + signal.addEventListener( + "abort", + () => { + upload.abort(); + const abortError = new DOMException( + "Tus upload aborted", + "AbortError", + ); + setUploadError(abortError); + setIsUploading(false); + activeUploadRef.current = null; + reject(abortError); + }, + { once: true }, + ); + } + + upload + .findPreviousUploads() + .then((previousUploads) => { + if (previousUploads.length > 0) { + upload.resumeFromPreviousUpload(previousUploads[0]); + } + upload.start(); + }) + .catch((resumeError) => { + const normalized = normalizeError( + resumeError, + "Failed to resume tus upload", + ); + setUploadError(normalized); + setIsUploading(false); + activeUploadRef.current = null; + reject(normalized); + }); + }); + + return result; + } finally { + activeUploadRef.current = null; + setIsUploading(false); + } + }, + ); + + const resetUploadError = () => setUploadError(null); + + const abortUpload = () => { + if (activeUploadRef.current) { + activeUploadRef.current.abort(); + activeUploadRef.current = null; + setIsUploading(false); + setUploadProgress(0); + } + }; + + return { + ...mutation, + uploadProgress, + isUploading, + uploadError, + resetUploadError, + abortUpload, + }; +}; + +export const useLegacyUploadFile = () => { const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); - const mutation = useMutation(["upload-file"], async (file: File) => { - console.log(`Начинаем загрузку файла: ${file.name} (${file.size} байт)`); + const mutation = useMutation(["upload-file", "legacy"], async (file: File) => { setIsUploading(true); setUploadProgress(0); - setUploadError(null); // Сбрасываем ошибку перед началом новой загрузки + setUploadError(null); try { - // Для маленьких файлов используем обычную загрузку, но с теми же заголовками if (file.size <= MAX_CHUNK_SIZE) { - console.log("Используем обычную загрузку (файл <= MAX_CHUNK_SIZE)"); - - // Подготавливаем заголовки - такие же, как для чанковой загрузки const headers: Record = { - "X-File-Name": btoa(unescape(encodeURIComponent(file.name))), // Имя файла в base64 - "X-Chunk-Start": "0", // Начинаем с позиции 0 + "X-File-Name": btoa(unescape(encodeURIComponent(file.name))), + "X-Chunk-Start": "0", "Content-Type": file.type || "application/octet-stream", - "X-Last-Chunk": "1" // Это единственный и последний чанк + "X-Last-Chunk": "1", }; - - // Добавляем заголовок авторизации - const authToken = localStorage.getItem('auth_v1_token'); + + const authToken = localStorage.getItem("auth_v1_token"); if (authToken) { - headers["Authorization"] = authToken; - } - - console.log("Заголовки запроса:", headers); - - try { - const response = await request.post<{ - upload_id?: string; - content_sha256?: string; - content_id?: string; - content_id_v1?: string; - content_url?: string; - }>("", file, { // Отправляем файл напрямую вместо FormData - baseURL: STORAGE_API_URL, - headers, - onUploadProgress: (progressEvent) => { - const percentCompleted = Math.round( - (progressEvent.loaded * 100) / (progressEvent?.total as number || file.size) - ); - setUploadProgress(Math.min(99, percentCompleted)); - }, - }); - - console.log("Ответ на обычную загрузку:", response.data); - setUploadProgress(100); - - return { - content_sha256: response.data.content_sha256 || "", - content_id_v1: response.data.content_id_v1 || response.data.content_id || "", - content_url: response.data.content_url || "" - }; - } catch (error) { - console.error("Ошибка при обычной загрузке:", error); - setUploadError(error instanceof Error ? error : new Error('Ошибка при загрузке файла')); - throw error; + headers.Authorization = authToken; } + + const response = await request.post<{ + upload_id?: string; + content_sha256?: string; + content_id?: string; + content_id_v1?: string; + content_url?: string; + }>("", file, { + baseURL: STORAGE_API_URL, + headers, + onUploadProgress: (progressEvent) => { + const total = progressEvent?.total ?? file.size; + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / total, + ); + setUploadProgress(Math.min(99, percentCompleted)); + }, + }); + + setUploadProgress(100); + + return { + content_sha256: response.data.content_sha256 || "", + content_id_v1: + response.data.content_id_v1 || response.data.content_id || "", + content_url: response.data.content_url || "", + }; } - - // Для больших файлов используем чанковую загрузку - console.log("Используем чанкированную загрузку (файл > MAX_CHUNK_SIZE)"); - + let offset = 0; let uploadId: string | null = null; - let chunkNumber = 0; - - // Загружаем файл по чанкам + while (offset < file.size) { - chunkNumber++; const chunkEnd = Math.min(offset + MAX_CHUNK_SIZE, file.size); const chunk = file.slice(offset, chunkEnd); - console.log(`Загрузка чанка #${chunkNumber} начиная с байта ${offset}`); - - // Определяем, является ли текущий чанк последним const isLastChunk = chunkEnd === file.size; - - // Подготавливаем заголовки + const headers: Record = { - "X-File-Name": btoa(unescape(encodeURIComponent(file.name))), // Имя файла в base64 + "X-File-Name": btoa(unescape(encodeURIComponent(file.name))), "X-Chunk-Start": offset.toString(), - "Content-Type": file.type || "application/octet-stream" + "Content-Type": file.type || "application/octet-stream", }; - - // Добавляем маркер последнего чанка, если это последний чанк + if (isLastChunk) { headers["X-Last-Chunk"] = "1"; } - - // Добавляем заголовок авторизации - const authToken = localStorage.getItem('auth_v1_token'); + + const authToken = localStorage.getItem("auth_v1_token"); if (authToken) { - headers["Authorization"] = authToken; + headers.Authorization = authToken; } - - // Если есть uploadId, добавляем его в заголовки + if (uploadId) { headers["X-Upload-ID"] = uploadId; } - - console.log("Заголовки запроса:", headers); - - try { - const response = await request.post<{ - upload_id?: string; - current_size?: number; - content_id?: string; - content_sha256?: string; - content_id_v1?: string; - content_url?: string; - }>("", chunk, { - baseURL: STORAGE_API_URL, - headers, - onUploadProgress: (progressEvent) => { - // Прогресс загрузки текущего чанка - const overallProgress = offset + progressEvent.loaded; - const percentCompleted = Math.round((overallProgress / file.size) * 100); - setUploadProgress(Math.min(99, percentCompleted)); - }, - }); - - console.log(`Ответ на чанк #${chunkNumber}:`, response.data); - - // Сохраняем uploadId из первого ответа, если не установлен - if (!uploadId && response.data.upload_id) { - uploadId = response.data.upload_id; - console.log("Получен upload_id:", uploadId); - } - - // Проверка на наличие content_id - if (response.data.content_id) { - console.log("Загрузка завершена. ID файла:", response.data.content_id); - setUploadProgress(100); - - return { - content_sha256: response.data.content_sha256 || "", - content_id_v1: response.data.content_id_v1 || response.data.content_id || "", - content_url: response.data.content_url || "" - }; - } - - // Обновляем смещение на основе ответа сервера - if (response.data.current_size !== undefined) { - offset = response.data.current_size; - console.log(`Сервер сообщает current_size: ${offset}`); - } else { - console.warn("Неожиданный ответ от сервера, отсутствует current_size"); - const error = new Error("Missing current_size in response"); - setUploadError(error); - throw error; - } - } catch (error: any) { - console.error(`Ошибка при загрузке чанка #${chunkNumber}:`, error); - if (error.response) { - console.error("Ответ сервера:", error.response.status, error.response.data); - } - setUploadError(error instanceof Error ? error : new Error(`Ошибка при загрузке чанка #${chunkNumber}`)); + + const response = await request.post<{ + upload_id?: string; + current_size?: number; + content_id?: string; + content_sha256?: string; + content_id_v1?: string; + content_url?: string; + }>("", chunk, { + baseURL: STORAGE_API_URL, + headers, + onUploadProgress: (progressEvent) => { + const overallProgress = offset + progressEvent.loaded; + const percentCompleted = Math.round( + (overallProgress / file.size) * 100, + ); + setUploadProgress(Math.min(99, percentCompleted)); + }, + }); + + if (!uploadId && response.data.upload_id) { + uploadId = response.data.upload_id; + } + + if (response.data.content_id) { + setUploadProgress(100); + + return { + content_sha256: response.data.content_sha256 || "", + content_id_v1: + response.data.content_id_v1 || response.data.content_id || "", + content_url: response.data.content_url || "", + }; + } + + if (response.data.current_size !== undefined) { + offset = response.data.current_size; + } else { + const error = new Error("Missing current_size in response"); + setUploadError(error); throw error; } } - - const error = new Error("Ошибка загрузки файла: все чанки загружены, но content_id не получен"); - console.error(error.message); + + const error = new Error( + "All chunks uploaded but server did not return content_id", + ); setUploadError(error); throw error; - } catch (error: any) { - console.error("Ошибка при загрузке:", error); - if (error.response) { - console.error("Ответ сервера:", error.response.status, error.response.data); - } - setUploadError(error instanceof Error ? error : new Error("Неизвестная ошибка при загрузке")); + } catch (error) { + const normalized = normalizeError( + error, + "Unknown error during legacy upload", + ); + setUploadError(normalized); setIsUploading(false); - throw error; + throw normalized; } finally { setIsUploading(false); } }); - // Сбросить ошибку const resetUploadError = () => setUploadError(null); - return { - ...mutation, - uploadProgress, - isUploading, + return { + ...mutation, + uploadProgress, + isUploading, uploadError, - resetUploadError + resetUploadError, }; -}; \ No newline at end of file +}; + +export { useTusUpload as useUploadFile }; +export type { TusUploadArgs, TusUploadResult, UploadSessionState }; diff --git a/yarn.lock b/yarn.lock index ea8fb39..b6c3bf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,10 +225,10 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@esbuild/win32-x64@0.19.12": +"@esbuild/darwin-arm64@0.19.12": version "0.19.12" - resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz" - integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz" + integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" @@ -381,10 +381,10 @@ resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz" integrity sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q== -"@rollup/rollup-win32-x64-msvc@4.12.0": +"@rollup/rollup-darwin-arm64@4.12.0": version "4.12.0" - resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz" - integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg== + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz" + integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ== "@sentry-internal/browser-utils@9.1.0": version "9.1.0" @@ -976,6 +976,11 @@ browserslist@^4.22.2, browserslist@^4.23.0, "browserslist@>= 4.21.0": node-releases "^2.0.14" update-browserslist-db "^1.0.13" +buffer-from@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + buffer@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" @@ -1081,6 +1086,14 @@ color-name@1.1.3: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +combine-errors@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/combine-errors/-/combine-errors-3.0.3.tgz" + integrity sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q== + dependencies: + custom-error-instance "2.1.1" + lodash.uniqby "4.5.0" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1122,6 +1135,11 @@ csstype@^3.0.2, csstype@^3.1.1: resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +custom-error-instance@2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz" + integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg== + debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -1637,6 +1655,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -1769,6 +1792,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphemer@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" @@ -1975,6 +2003,11 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" @@ -2027,6 +2060,11 @@ jiti@^1.19.1: resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +js-base64@^3.7.2: + version "3.7.8" + resolved "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz" + integrity sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow== + js-sha3@0.8.0: version "0.8.0" resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz" @@ -2076,11 +2114,6 @@ json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jssha@^3.3.1: - version "3.3.1" - resolved "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz" - integrity sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ== - jssha@3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz" @@ -2133,11 +2166,61 @@ lodash-es@^4.17.21: resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +lodash._baseiteratee@~4.7.0: + version "4.7.0" + resolved "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz" + integrity sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ== + dependencies: + lodash._stringtopath "~4.8.0" + +lodash._basetostring@~4.12.0: + version "4.12.0" + resolved "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz" + integrity sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw== + +lodash._baseuniq@~4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz" + integrity sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A== + dependencies: + lodash._createset "~4.0.0" + lodash._root "~3.0.0" + +lodash._createset@~4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz" + integrity sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA== + +lodash._root@~3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz" + integrity sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ== + +lodash._stringtopath@~4.8.0: + version "4.8.0" + resolved "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz" + integrity sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ== + dependencies: + lodash._basetostring "~4.12.0" + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +lodash.uniqby@4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz" + integrity sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ== + dependencies: + lodash._baseiteratee "~4.7.0" + lodash._baseuniq "~4.6.0" + loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" @@ -2544,6 +2627,15 @@ prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.13.1" +proper-lockfile@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" @@ -2554,6 +2646,11 @@ punycode@^2.1.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -2697,6 +2794,11 @@ remove-accents@0.5.0: resolved "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz" integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -2711,6 +2813,11 @@ resolve@^1.1.7, resolve@^1.22.2, resolve@^1.22.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -2834,6 +2941,11 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" @@ -3080,6 +3192,19 @@ tslib@^2.6.2: resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tus-js-client@^4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/tus-js-client/-/tus-js-client-4.3.1.tgz" + integrity sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg== + dependencies: + buffer-from "^1.1.2" + combine-errors "^3.0.3" + is-stream "^2.0.0" + js-base64 "^3.7.2" + lodash.throttle "^4.1.1" + proper-lockfile "^4.1.2" + url-parse "^1.5.7" + tweetnacl-util@^0.15.1: version "0.15.1" resolved "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz" @@ -3194,6 +3319,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@^1.5.7: + version "1.5.10" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + use-sync-external-store@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"