@internxt/cli 1.6.8 → 1.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -28
- package/bin/dev.js +10 -0
- package/bin/run.js +10 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/move-file.d.ts +2 -0
- package/dist/commands/trash-list.d.ts +1 -2
- package/dist/commands/trash-restore-file.d.ts +2 -0
- package/dist/commands/upload-file.d.ts +2 -2
- package/dist/services/database/database.service.d.ts +6 -0
- package/dist/services/database/database.service.js +27 -0
- package/dist/services/database/drive-item/drive-item.repository.d.ts +2 -1
- package/dist/services/database/drive-item/drive-item.repository.js +10 -5
- package/dist/services/drive/drive-file.service.js +4 -6
- package/dist/services/drive/drive-folder.service.d.ts +9 -5
- package/dist/services/drive/drive-folder.service.js +48 -31
- package/dist/services/drive/drive-item.service.d.ts +1 -0
- package/dist/services/drive/drive-item.service.js +24 -14
- package/dist/services/drive/trash.service.d.ts +5 -4
- package/dist/services/drive/trash.service.js +21 -22
- package/dist/services/webdav/webdav-folder.service.d.ts +4 -0
- package/dist/services/webdav/webdav-folder.service.js +14 -1
- package/dist/types/config.types.d.ts +1 -0
- package/dist/types/drive.types.d.ts +1 -0
- package/dist/utils/cli.utils.js +2 -8
- package/dist/utils/drive.utils.d.ts +1 -0
- package/dist/utils/drive.utils.js +9 -1
- package/dist/utils/errors.utils.d.ts +35 -0
- package/dist/utils/errors.utils.js +116 -3
- package/dist/utils/network.utils.js +7 -6
- package/dist/utils/pagination.utils.d.ts +7 -0
- package/dist/utils/pagination.utils.js +20 -0
- package/dist/utils/webdav.utils.js +30 -17
- package/dist/webdav/handlers/DELETE.handler.js +0 -2
- package/dist/webdav/handlers/GET.handler.js +15 -5
- package/dist/webdav/handlers/HEAD.handler.js +17 -7
- package/dist/webdav/handlers/LOCK.handler.js +5 -1
- package/dist/webdav/handlers/MKCOL.handler.js +7 -1
- package/dist/webdav/handlers/MOVE.handler.js +13 -1
- package/dist/webdav/handlers/OPTIONS.handler.js +3 -3
- package/dist/webdav/handlers/PROPFIND.handler.js +2 -2
- package/dist/webdav/handlers/PROPPATCH.handler.d.ts +3 -1
- package/dist/webdav/handlers/PROPPATCH.handler.js +53 -3
- package/dist/webdav/handlers/PUT.handler.js +2 -7
- package/dist/webdav/index.js +3 -2
- package/dist/webdav/middewares/auth.middleware.js +3 -2
- package/dist/webdav/middewares/errors.middleware.js +10 -14
- package/oclif.manifest.json +1 -1
- package/package.json +31 -26
|
@@ -8,6 +8,11 @@ const drive_file_service_1 = require("./drive-file.service");
|
|
|
8
8
|
const drive_folder_service_1 = require("./drive-folder.service");
|
|
9
9
|
class DriveItemService {
|
|
10
10
|
static instance = new DriveItemService();
|
|
11
|
+
dropCacheIfGone = async (uuid, error) => {
|
|
12
|
+
if (!errors_utils_1.ErrorUtils.isNotFoundError(error))
|
|
13
|
+
return;
|
|
14
|
+
await drive_item_repository_1.DriveItemRepository.instance.delete([uuid]);
|
|
15
|
+
};
|
|
11
16
|
tryGetFileByUuid = async (cached, path) => {
|
|
12
17
|
try {
|
|
13
18
|
const item = await drive_file_service_1.DriveFileService.instance.getFileMetadata(cached.uuid);
|
|
@@ -22,9 +27,12 @@ class DriveItemService {
|
|
|
22
27
|
]);
|
|
23
28
|
return item;
|
|
24
29
|
}
|
|
25
|
-
catch {
|
|
26
|
-
logger_utils_1.
|
|
27
|
-
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger_utils_1.webdavLogger.warn(errors_utils_1.ErrorUtils.withRequestId('File metadata by uuid failed, falling back to path lookup', error), {
|
|
32
|
+
path,
|
|
33
|
+
uuid: cached.uuid,
|
|
34
|
+
});
|
|
35
|
+
await this.dropCacheIfGone(cached.uuid, error);
|
|
28
36
|
}
|
|
29
37
|
};
|
|
30
38
|
tryGetFolderByUuid = async (cached, path) => {
|
|
@@ -41,14 +49,14 @@ class DriveItemService {
|
|
|
41
49
|
]);
|
|
42
50
|
return item;
|
|
43
51
|
}
|
|
44
|
-
catch {
|
|
45
|
-
logger_utils_1.
|
|
46
|
-
await
|
|
52
|
+
catch (error) {
|
|
53
|
+
logger_utils_1.webdavLogger.warn(errors_utils_1.ErrorUtils.withRequestId('Folder metadata by uuid failed, falling back to path lookup', error), { path, uuid: cached.uuid });
|
|
54
|
+
await this.dropCacheIfGone(cached.uuid, error);
|
|
47
55
|
}
|
|
48
56
|
};
|
|
49
57
|
getFileByPath = async (path) => {
|
|
50
|
-
const cached = await drive_item_repository_1.DriveItemRepository.instance.getByPath(path);
|
|
51
|
-
if (cached) {
|
|
58
|
+
const cached = await drive_item_repository_1.DriveItemRepository.instance.getByPath(path, 'file');
|
|
59
|
+
if (cached?.type === 'file') {
|
|
52
60
|
const item = await this.tryGetFileByUuid(cached, path);
|
|
53
61
|
if (item)
|
|
54
62
|
return item;
|
|
@@ -66,13 +74,14 @@ class DriveItemService {
|
|
|
66
74
|
]);
|
|
67
75
|
return item;
|
|
68
76
|
}
|
|
69
|
-
catch {
|
|
70
|
-
|
|
77
|
+
catch (error) {
|
|
78
|
+
errors_utils_1.ErrorUtils.logIfUnexpected(logger_utils_1.webdavLogger, 'File lookup by path failed', error, { path });
|
|
79
|
+
throw errors_utils_1.ErrorUtils.toLookupError('file', path, error);
|
|
71
80
|
}
|
|
72
81
|
};
|
|
73
82
|
getFolderByPath = async (path) => {
|
|
74
|
-
const cached = await drive_item_repository_1.DriveItemRepository.instance.getByPath(path);
|
|
75
|
-
if (cached) {
|
|
83
|
+
const cached = await drive_item_repository_1.DriveItemRepository.instance.getByPath(path, 'folder');
|
|
84
|
+
if (cached?.type === 'folder') {
|
|
76
85
|
const item = await this.tryGetFolderByUuid(cached, path);
|
|
77
86
|
if (item)
|
|
78
87
|
return item;
|
|
@@ -90,8 +99,9 @@ class DriveItemService {
|
|
|
90
99
|
]);
|
|
91
100
|
return item;
|
|
92
101
|
}
|
|
93
|
-
catch {
|
|
94
|
-
|
|
102
|
+
catch (error) {
|
|
103
|
+
errors_utils_1.ErrorUtils.logIfUnexpected(logger_utils_1.webdavLogger, 'Folder lookup by path failed', error, { path });
|
|
104
|
+
throw errors_utils_1.ErrorUtils.toLookupError('folder', path, error);
|
|
95
105
|
}
|
|
96
106
|
};
|
|
97
107
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StorageTypes } from '@internxt/sdk/dist/drive';
|
|
2
2
|
export declare class TrashService {
|
|
3
3
|
static readonly instance: TrashService;
|
|
4
|
+
private static readonly SCAN_FROM;
|
|
4
5
|
trashItems: (payload: StorageTypes.AddItemsToTrashPayload) => Promise<void>;
|
|
5
6
|
deleteItemPermanently: (itemType: "file" | "folder", id: string) => Promise<void> | Promise<{
|
|
6
7
|
deleted: boolean;
|
|
@@ -31,7 +32,6 @@ export declare class TrashService {
|
|
|
31
32
|
status: "EXISTS" | "TRASHED" | "DELETED";
|
|
32
33
|
removed: boolean;
|
|
33
34
|
deleted: boolean;
|
|
34
|
-
isFavorite?: boolean;
|
|
35
35
|
}[];
|
|
36
36
|
files: {
|
|
37
37
|
id: number;
|
|
@@ -51,9 +51,10 @@ export declare class TrashService {
|
|
|
51
51
|
updatedAt: string;
|
|
52
52
|
plainName: string;
|
|
53
53
|
status: "EXISTS" | "TRASHED" | "DELETED";
|
|
54
|
-
|
|
54
|
+
sharings?: import("@internxt/sdk/dist/schema").components["schemas"]["FileSharingDto"][];
|
|
55
55
|
}[];
|
|
56
56
|
}>;
|
|
57
|
-
private readonly
|
|
58
|
-
private readonly
|
|
57
|
+
private readonly getTrashedFolders;
|
|
58
|
+
private readonly getTrashedFiles;
|
|
59
|
+
private readonly syncQuery;
|
|
59
60
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TrashService = void 0;
|
|
4
4
|
const sdk_manager_service_1 = require("../sdk-manager.service");
|
|
5
|
+
const pagination_utils_1 = require("../../utils/pagination.utils");
|
|
5
6
|
class TrashService {
|
|
6
7
|
static instance = new TrashService();
|
|
8
|
+
static SCAN_FROM = new Date(0).toISOString();
|
|
7
9
|
trashItems = (payload) => {
|
|
8
10
|
const storageClient = sdk_manager_service_1.SdkManager.instance.getStorage();
|
|
9
11
|
return storageClient.addItemsToTrash(payload);
|
|
@@ -29,31 +31,28 @@ class TrashService {
|
|
|
29
31
|
return trashClient.clearTrash();
|
|
30
32
|
};
|
|
31
33
|
getTrashFolderContent = async () => {
|
|
32
|
-
const folders = await this.
|
|
33
|
-
const files = await this.getAllTrashSubfiles(0);
|
|
34
|
+
const [folders, files] = await Promise.all([this.getTrashedFolders(), this.getTrashedFiles()]);
|
|
34
35
|
return { folders, files };
|
|
35
36
|
};
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return folders
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
return folders;
|
|
45
|
-
}
|
|
37
|
+
getTrashedFolders = async () => {
|
|
38
|
+
const storageClient = sdk_manager_service_1.SdkManager.instance.getStorage();
|
|
39
|
+
return pagination_utils_1.PaginationUtils.fetchAllPages(async (cursor) => {
|
|
40
|
+
const [promise] = storageClient.getFoldersSync(this.syncQuery(cursor));
|
|
41
|
+
const { folders, nextCursor } = await promise;
|
|
42
|
+
return { items: folders, nextCursor };
|
|
43
|
+
});
|
|
46
44
|
};
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return files
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
45
|
+
getTrashedFiles = async () => {
|
|
46
|
+
const storageClient = sdk_manager_service_1.SdkManager.instance.getStorage();
|
|
47
|
+
return pagination_utils_1.PaginationUtils.fetchAllPages(async (cursor) => {
|
|
48
|
+
const [promise] = storageClient.getFilesSync(this.syncQuery(cursor));
|
|
49
|
+
const { files, nextCursor } = await promise;
|
|
50
|
+
return { items: files, nextCursor };
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
syncQuery = (cursor) => {
|
|
54
|
+
const position = cursor ? { cursor } : { updatedAt: TrashService.SCAN_FROM };
|
|
55
|
+
return { ...position, status: 'TRASHED', limit: pagination_utils_1.PaginationUtils.MAX_PAGE_SIZE };
|
|
57
56
|
};
|
|
58
57
|
}
|
|
59
58
|
exports.TrashService = TrashService;
|
|
@@ -6,6 +6,10 @@ export declare class WebDavFolderService {
|
|
|
6
6
|
folderName: string;
|
|
7
7
|
parentFolderUuid: string;
|
|
8
8
|
}) => Promise<DriveFolderItem>;
|
|
9
|
+
createOrReuseFolder: ({ folderName, parentFolderUuid, }: {
|
|
10
|
+
folderName: string;
|
|
11
|
+
parentFolderUuid: string;
|
|
12
|
+
}) => Promise<DriveFolderItem>;
|
|
9
13
|
createParentPathOrThrow: (parentPath: string) => Promise<DriveFolderItem>;
|
|
10
14
|
private createFolderRecursively;
|
|
11
15
|
}
|
|
@@ -23,6 +23,19 @@ class WebDavFolderService {
|
|
|
23
23
|
await async_utils_1.AsyncUtils.sleep(500);
|
|
24
24
|
return drive_utils_1.DriveUtils.createFolderResponseToItem(newFolder);
|
|
25
25
|
};
|
|
26
|
+
createOrReuseFolder = async ({ folderName, parentFolderUuid, }) => {
|
|
27
|
+
try {
|
|
28
|
+
return await this.createFolder({ folderName, parentFolderUuid });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (!errors_utils_1.ErrorUtils.isAlreadyExistsError(error))
|
|
32
|
+
throw error;
|
|
33
|
+
const existingFolder = await drive_folder_service_1.DriveFolderService.instance.findExistentFolder(parentFolderUuid, folderName);
|
|
34
|
+
if (!existingFolder)
|
|
35
|
+
throw error;
|
|
36
|
+
return existingFolder;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
26
39
|
createParentPathOrThrow = async (parentPath) => {
|
|
27
40
|
const { createFullPath } = await config_service_1.ConfigService.instance.readWebdavConfig();
|
|
28
41
|
if (!createFullPath) {
|
|
@@ -38,7 +51,7 @@ class WebDavFolderService {
|
|
|
38
51
|
const newPath = webdav_utils_1.WebDavUtils.joinURL(accumulatedPath, currentFolderName);
|
|
39
52
|
const folderPath = webdav_utils_1.WebDavUtils.normalizeFolderPath(newPath);
|
|
40
53
|
const folder = (await this.getDriveFolderItemFromPath(folderPath)) ??
|
|
41
|
-
(await this.
|
|
54
|
+
(await this.createOrReuseFolder({ folderName: currentFolderName, parentFolderUuid }));
|
|
42
55
|
if (rest.length === 0) {
|
|
43
56
|
return folder;
|
|
44
57
|
}
|
package/dist/utils/cli.utils.js
CHANGED
|
@@ -185,13 +185,7 @@ class CLIUtils {
|
|
|
185
185
|
};
|
|
186
186
|
static catchError = ({ error, logReporter, command, jsonFlag, debugMode, }) => {
|
|
187
187
|
let message = '';
|
|
188
|
-
|
|
189
|
-
if ('requestId' in error) {
|
|
190
|
-
requestId = error.requestId;
|
|
191
|
-
}
|
|
192
|
-
else if ('xRequestId' in error) {
|
|
193
|
-
requestId = error.xRequestId;
|
|
194
|
-
}
|
|
188
|
+
const requestId = errors_utils_1.ErrorUtils.getRequestId(error);
|
|
195
189
|
if ('message' in error && typeof error.message === 'string' && error.message?.trim?.().length > 0) {
|
|
196
190
|
message = error.message;
|
|
197
191
|
}
|
|
@@ -226,7 +220,7 @@ class CLIUtils {
|
|
|
226
220
|
if (debugMode) {
|
|
227
221
|
errors_utils_1.ErrorUtils.report(error);
|
|
228
222
|
}
|
|
229
|
-
CLIUtils.error(logReporter, message
|
|
223
|
+
CLIUtils.error(logReporter, errors_utils_1.ErrorUtils.withRequestId(message, error));
|
|
230
224
|
}
|
|
231
225
|
};
|
|
232
226
|
static parseEmpty = async (input) => (input.trim().length === 0 ? ' ' : input);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FileMeta, FolderMeta, CreateFolderResponse } from '@internxt/sdk/dist/drive/storage/types';
|
|
2
2
|
import { DriveFileItem, DriveFolderItem } from '../types/drive.types';
|
|
3
3
|
export declare class DriveUtils {
|
|
4
|
+
private static assertUsableMeta;
|
|
4
5
|
static parseFileSize(size: string | number): number;
|
|
5
6
|
static driveFileMetaToItem(fileMeta: FileMeta): DriveFileItem;
|
|
6
7
|
static driveFolderMetaToItem(folderMeta: FolderMeta): DriveFolderItem;
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DriveUtils = void 0;
|
|
4
4
|
const types_1 = require("@internxt/sdk/dist/drive/storage/types");
|
|
5
|
+
const errors_utils_1 = require("./errors.utils");
|
|
5
6
|
class DriveUtils {
|
|
7
|
+
static assertUsableMeta(meta, kind) {
|
|
8
|
+
if (!meta?.uuid) {
|
|
9
|
+
throw new errors_utils_1.ServiceUnavailableError(`Unusable ${kind} metadata received from the API`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
6
12
|
static parseFileSize(size) {
|
|
7
13
|
const parsed = Number(size);
|
|
8
14
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
9
15
|
}
|
|
10
16
|
static driveFileMetaToItem(fileMeta) {
|
|
17
|
+
DriveUtils.assertUsableMeta(fileMeta, 'file');
|
|
11
18
|
return {
|
|
12
19
|
itemType: 'file',
|
|
13
|
-
uuid: fileMeta.uuid
|
|
20
|
+
uuid: fileMeta.uuid,
|
|
14
21
|
status: fileMeta.status,
|
|
15
22
|
folderUuid: fileMeta.folderUuid,
|
|
16
23
|
size: DriveUtils.parseFileSize(fileMeta.size),
|
|
@@ -25,6 +32,7 @@ class DriveUtils {
|
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
static driveFolderMetaToItem(folderMeta) {
|
|
35
|
+
DriveUtils.assertUsableMeta(folderMeta, 'folder');
|
|
28
36
|
return {
|
|
29
37
|
itemType: 'folder',
|
|
30
38
|
uuid: folderMeta.uuid,
|
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
import { Logger } from 'winston';
|
|
2
|
+
import { DriveItemType } from '../types/drive.types';
|
|
3
|
+
export type LookupErrorKind = 'not-found' | 'auth' | 'inconclusive';
|
|
4
|
+
export declare const DEFAULT_RETRY_AFTER_SECONDS = 5;
|
|
1
5
|
export declare class ErrorUtils {
|
|
2
6
|
static readonly isError: (error: unknown) => error is Error;
|
|
3
7
|
static readonly report: (error: unknown, props?: Record<string, unknown>) => void;
|
|
4
8
|
static readonly isAlreadyExistsError: (error: unknown) => error is Error;
|
|
5
9
|
static readonly isFileNotFoundError: (error: unknown) => error is NodeJS.ErrnoException;
|
|
10
|
+
static readonly getRequestId: (error: unknown) => string | undefined;
|
|
11
|
+
static readonly withRequestId: (message: string, error: unknown) => string;
|
|
12
|
+
static readonly getStatusCode: (error: unknown, key?: "statusCode" | "status") => number | undefined;
|
|
13
|
+
static readonly isNotFoundError: (error: unknown) => boolean;
|
|
14
|
+
private static readonly hasResponseBody;
|
|
15
|
+
static readonly classifyLookupError: (error: unknown) => LookupErrorKind;
|
|
16
|
+
static readonly toLookupError: (itemType: DriveItemType, path: string, error: unknown) => Error;
|
|
17
|
+
private static readonly isAuthStatus;
|
|
18
|
+
private static readonly isRetryableStatus;
|
|
19
|
+
static readonly toWebDavStatus: (error: unknown) => {
|
|
20
|
+
statusCode: number;
|
|
21
|
+
retryAfter?: number;
|
|
22
|
+
};
|
|
23
|
+
static readonly logIfUnexpected: (log: Logger, message: string, error: unknown, meta?: Record<string, unknown>) => void;
|
|
6
24
|
}
|
|
7
25
|
export declare class ConflictError extends Error {
|
|
8
26
|
statusCode: number;
|
|
@@ -12,6 +30,15 @@ export declare class NotFoundError extends Error {
|
|
|
12
30
|
statusCode: number;
|
|
13
31
|
constructor(message: string);
|
|
14
32
|
}
|
|
33
|
+
export declare class ServiceUnavailableError extends Error {
|
|
34
|
+
statusCode: number;
|
|
35
|
+
retryAfter: number;
|
|
36
|
+
constructor(message: string, retryAfter?: number);
|
|
37
|
+
}
|
|
38
|
+
export declare class BadGatewayError extends Error {
|
|
39
|
+
statusCode: number;
|
|
40
|
+
constructor(message: string);
|
|
41
|
+
}
|
|
15
42
|
export declare class BadRequestError extends Error {
|
|
16
43
|
statusCode: number;
|
|
17
44
|
constructor(message: string);
|
|
@@ -28,3 +55,11 @@ export declare class NotImplementedError extends Error {
|
|
|
28
55
|
statusCode: number;
|
|
29
56
|
constructor(message: string);
|
|
30
57
|
}
|
|
58
|
+
export declare class PreconditionFailedError extends Error {
|
|
59
|
+
statusCode: number;
|
|
60
|
+
constructor(message: string);
|
|
61
|
+
}
|
|
62
|
+
export declare class RangeNotSatisfiableError extends Error {
|
|
63
|
+
statusCode: number;
|
|
64
|
+
constructor(message: string);
|
|
65
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NotImplementedError = exports.MethodNotAllowed = exports.UnsupportedMediaTypeError = exports.BadRequestError = exports.NotFoundError = exports.ConflictError = exports.ErrorUtils = void 0;
|
|
3
|
+
exports.RangeNotSatisfiableError = exports.PreconditionFailedError = exports.NotImplementedError = exports.MethodNotAllowed = exports.UnsupportedMediaTypeError = exports.BadRequestError = exports.BadGatewayError = exports.ServiceUnavailableError = exports.NotFoundError = exports.ConflictError = exports.ErrorUtils = exports.DEFAULT_RETRY_AFTER_SECONDS = void 0;
|
|
4
4
|
const logger_utils_1 = require("./logger.utils");
|
|
5
|
+
exports.DEFAULT_RETRY_AFTER_SECONDS = 5;
|
|
5
6
|
class ErrorUtils {
|
|
6
7
|
static isError = (error) => {
|
|
7
8
|
return typeof Error.isError === 'function'
|
|
@@ -11,10 +12,12 @@ class ErrorUtils {
|
|
|
11
12
|
};
|
|
12
13
|
static report = (error, props = {}) => {
|
|
13
14
|
if (this.isError(error)) {
|
|
14
|
-
logger_utils_1.logger.error(`[REPORTED_ERROR]: ${error.message
|
|
15
|
+
logger_utils_1.logger.error(`[REPORTED_ERROR]: ${this.withRequestId(error.message, error)}\n` +
|
|
16
|
+
`Properties => ${JSON.stringify(props, null, 2)}\nStack => ${error.stack}`);
|
|
15
17
|
}
|
|
16
18
|
else {
|
|
17
|
-
logger_utils_1.logger.error(`[REPORTED_ERROR]: ${JSON.stringify(error)
|
|
19
|
+
logger_utils_1.logger.error(`[REPORTED_ERROR]: ${this.withRequestId(JSON.stringify(error), error)}\n` +
|
|
20
|
+
`Properties => ${JSON.stringify(props, null, 2)}\n`);
|
|
18
21
|
}
|
|
19
22
|
};
|
|
20
23
|
static isAlreadyExistsError = (error) => {
|
|
@@ -24,6 +27,78 @@ class ErrorUtils {
|
|
|
24
27
|
static isFileNotFoundError = (error) => {
|
|
25
28
|
return this.isError(error) && 'code' in error && error.code === 'ENOENT';
|
|
26
29
|
};
|
|
30
|
+
static getRequestId = (error) => {
|
|
31
|
+
if (typeof error !== 'object' || error === null)
|
|
32
|
+
return undefined;
|
|
33
|
+
const { requestId, xRequestId, data } = error;
|
|
34
|
+
const bodyRequestId = typeof data === 'object' && data !== null ? data.requestId : undefined;
|
|
35
|
+
return [requestId, xRequestId, bodyRequestId].find((candidate) => typeof candidate === 'string' && candidate.length > 0);
|
|
36
|
+
};
|
|
37
|
+
static withRequestId = (message, error) => {
|
|
38
|
+
const requestId = this.getRequestId(error);
|
|
39
|
+
return requestId ? `${message} (requestId: ${requestId})` : message;
|
|
40
|
+
};
|
|
41
|
+
static getStatusCode = (error, key) => {
|
|
42
|
+
if (typeof error !== 'object' || error === null)
|
|
43
|
+
return undefined;
|
|
44
|
+
const source = error;
|
|
45
|
+
const value = key ? source[key] : (source.statusCode ?? source.status);
|
|
46
|
+
return typeof value === 'number' && !Number.isNaN(value) ? value : undefined;
|
|
47
|
+
};
|
|
48
|
+
static isNotFoundError = (error) => {
|
|
49
|
+
return this.getStatusCode(error) === 404;
|
|
50
|
+
};
|
|
51
|
+
static hasResponseBody = (error) => {
|
|
52
|
+
return typeof error === 'object' && error !== null && 'data' in error;
|
|
53
|
+
};
|
|
54
|
+
static classifyLookupError = (error) => {
|
|
55
|
+
const status = this.getStatusCode(error);
|
|
56
|
+
if (status === 404)
|
|
57
|
+
return 'not-found';
|
|
58
|
+
if (status === undefined || !this.hasResponseBody(error))
|
|
59
|
+
return 'inconclusive';
|
|
60
|
+
if (status === 400 || status === 414 || status === 422)
|
|
61
|
+
return 'not-found';
|
|
62
|
+
if (this.isAuthStatus(status))
|
|
63
|
+
return 'auth';
|
|
64
|
+
return 'inconclusive';
|
|
65
|
+
};
|
|
66
|
+
static toLookupError = (itemType, path, error) => {
|
|
67
|
+
const label = itemType === 'file' ? 'File' : 'Folder';
|
|
68
|
+
switch (this.classifyLookupError(error)) {
|
|
69
|
+
case 'auth':
|
|
70
|
+
return new BadGatewayError(this.withRequestId(`The Internxt API rejected this session while looking up ${path}, log in again with 'internxt login'`, error));
|
|
71
|
+
case 'inconclusive':
|
|
72
|
+
return new ServiceUnavailableError(this.withRequestId(`${label} lookup at path ${path} could not be completed, retry later`, error));
|
|
73
|
+
case 'not-found':
|
|
74
|
+
return new NotFoundError(`${label} not found at path: ${path}`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
static isAuthStatus = (status) => status === 401 || status === 403;
|
|
78
|
+
static isRetryableStatus = (status) => status === 408 || status === 425 || status === 429 || status >= 500;
|
|
79
|
+
static toWebDavStatus = (error) => {
|
|
80
|
+
const ownStatus = this.getStatusCode(error, 'statusCode');
|
|
81
|
+
if (ownStatus !== undefined) {
|
|
82
|
+
return error instanceof ServiceUnavailableError
|
|
83
|
+
? { statusCode: ownStatus, retryAfter: error.retryAfter }
|
|
84
|
+
: { statusCode: ownStatus };
|
|
85
|
+
}
|
|
86
|
+
const apiStatus = this.getStatusCode(error, 'status');
|
|
87
|
+
if (apiStatus === undefined)
|
|
88
|
+
return { statusCode: 500 };
|
|
89
|
+
if (!this.hasResponseBody(error) || this.isRetryableStatus(apiStatus)) {
|
|
90
|
+
return { statusCode: 503, retryAfter: exports.DEFAULT_RETRY_AFTER_SECONDS };
|
|
91
|
+
}
|
|
92
|
+
if (this.isAuthStatus(apiStatus))
|
|
93
|
+
return { statusCode: 502 };
|
|
94
|
+
return { statusCode: apiStatus };
|
|
95
|
+
};
|
|
96
|
+
static logIfUnexpected = (log, message, error, meta) => {
|
|
97
|
+
if (this.isNotFoundError(error))
|
|
98
|
+
return;
|
|
99
|
+
const errorMessage = this.isError(error) ? error.message : String(error);
|
|
100
|
+
log.warn(this.withRequestId(`${message}: ${errorMessage}`, error), meta);
|
|
101
|
+
};
|
|
27
102
|
}
|
|
28
103
|
exports.ErrorUtils = ErrorUtils;
|
|
29
104
|
class ConflictError extends Error {
|
|
@@ -44,6 +119,26 @@ class NotFoundError extends Error {
|
|
|
44
119
|
}
|
|
45
120
|
}
|
|
46
121
|
exports.NotFoundError = NotFoundError;
|
|
122
|
+
class ServiceUnavailableError extends Error {
|
|
123
|
+
statusCode = 503;
|
|
124
|
+
retryAfter;
|
|
125
|
+
constructor(message, retryAfter = exports.DEFAULT_RETRY_AFTER_SECONDS) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.name = 'ServiceUnavailableError';
|
|
128
|
+
this.retryAfter = retryAfter;
|
|
129
|
+
Object.setPrototypeOf(this, ServiceUnavailableError.prototype);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.ServiceUnavailableError = ServiceUnavailableError;
|
|
133
|
+
class BadGatewayError extends Error {
|
|
134
|
+
statusCode = 502;
|
|
135
|
+
constructor(message) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = 'BadGatewayError';
|
|
138
|
+
Object.setPrototypeOf(this, BadGatewayError.prototype);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.BadGatewayError = BadGatewayError;
|
|
47
142
|
class BadRequestError extends Error {
|
|
48
143
|
statusCode = 400;
|
|
49
144
|
constructor(message) {
|
|
@@ -80,3 +175,21 @@ class NotImplementedError extends Error {
|
|
|
80
175
|
}
|
|
81
176
|
}
|
|
82
177
|
exports.NotImplementedError = NotImplementedError;
|
|
178
|
+
class PreconditionFailedError extends Error {
|
|
179
|
+
statusCode = 412;
|
|
180
|
+
constructor(message) {
|
|
181
|
+
super(message);
|
|
182
|
+
this.name = 'PreconditionFailedError';
|
|
183
|
+
Object.setPrototypeOf(this, PreconditionFailedError.prototype);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.PreconditionFailedError = PreconditionFailedError;
|
|
187
|
+
class RangeNotSatisfiableError extends Error {
|
|
188
|
+
statusCode = 416;
|
|
189
|
+
constructor(message) {
|
|
190
|
+
super(message);
|
|
191
|
+
this.name = 'RangeNotSatisfiableError';
|
|
192
|
+
Object.setPrototypeOf(this, RangeNotSatisfiableError.prototype);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.RangeNotSatisfiableError = RangeNotSatisfiableError;
|
|
@@ -10,6 +10,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
10
|
const selfsigned_1 = __importDefault(require("selfsigned"));
|
|
11
11
|
const range_parser_1 = __importDefault(require("range-parser"));
|
|
12
12
|
const configs_1 = require("../constants/configs");
|
|
13
|
+
const errors_utils_1 = require("./errors.utils");
|
|
13
14
|
class NetworkUtils {
|
|
14
15
|
static getAuthFromCredentials(creds) {
|
|
15
16
|
return {
|
|
@@ -81,10 +82,10 @@ class NetworkUtils {
|
|
|
81
82
|
const parsed = (0, range_parser_1.default)(rangeOptions.totalFileSize, rangeOptions.range);
|
|
82
83
|
if (Array.isArray(parsed)) {
|
|
83
84
|
if (parsed.length > 1) {
|
|
84
|
-
throw new
|
|
85
|
+
throw new errors_utils_1.BadRequestError(`Multi Range-Requests functionality is not implemented. ${JSON.stringify(rangeOptions)}`);
|
|
85
86
|
}
|
|
86
87
|
else if (parsed.length <= 0) {
|
|
87
|
-
throw new
|
|
88
|
+
throw new errors_utils_1.RangeNotSatisfiableError(`Empty Range-Request. ${JSON.stringify(rangeOptions)}`);
|
|
88
89
|
}
|
|
89
90
|
if (parsed.type === 'bytes') {
|
|
90
91
|
const rangeSize = parsed[0].end - parsed[0].start + 1;
|
|
@@ -96,17 +97,17 @@ class NetworkUtils {
|
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
99
|
else {
|
|
99
|
-
throw new
|
|
100
|
+
throw new errors_utils_1.BadRequestError(`Unkwnown Range-Request type "${parsed.type}". ${JSON.stringify(rangeOptions)}`);
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
else if (parsed === -1) {
|
|
103
|
-
throw new
|
|
104
|
+
throw new errors_utils_1.RangeNotSatisfiableError(`Unsatisfiable Range-Request. ${JSON.stringify(rangeOptions)}`);
|
|
104
105
|
}
|
|
105
106
|
else if (parsed === -2) {
|
|
106
|
-
throw new
|
|
107
|
+
throw new errors_utils_1.BadRequestError(`Malformed Range-Request. ${JSON.stringify(rangeOptions)}`);
|
|
107
108
|
}
|
|
108
109
|
else {
|
|
109
|
-
throw new
|
|
110
|
+
throw new errors_utils_1.BadRequestError(`Unknown error from Range-Request. ${JSON.stringify(rangeOptions)}`);
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PaginationUtils = void 0;
|
|
4
|
+
class PaginationUtils {
|
|
5
|
+
static MAX_PAGE_SIZE = 1000;
|
|
6
|
+
static fetchAllPages = async (fetchPage) => {
|
|
7
|
+
const items = [];
|
|
8
|
+
let cursor;
|
|
9
|
+
do {
|
|
10
|
+
const page = await fetchPage(cursor);
|
|
11
|
+
if (!Array.isArray(page.items)) {
|
|
12
|
+
throw new Error('Unusable page received from the API');
|
|
13
|
+
}
|
|
14
|
+
items.push(...page.items);
|
|
15
|
+
cursor = page.nextCursor ?? undefined;
|
|
16
|
+
} while (cursor);
|
|
17
|
+
return items;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
exports.PaginationUtils = PaginationUtils;
|
|
@@ -12,6 +12,7 @@ const config_service_1 = require("../services/config.service");
|
|
|
12
12
|
const trash_service_1 = require("../services/drive/trash.service");
|
|
13
13
|
const format_utils_1 = require("./format.utils");
|
|
14
14
|
const drive_item_repository_1 = require("../services/database/drive-item/drive-item.repository");
|
|
15
|
+
const errors_utils_1 = require("./errors.utils");
|
|
15
16
|
class WebDavUtils {
|
|
16
17
|
static joinURL(...pathComponents) {
|
|
17
18
|
return node_path_1.default.posix.join(...pathComponents);
|
|
@@ -54,35 +55,47 @@ class WebDavUtils {
|
|
|
54
55
|
try {
|
|
55
56
|
return await drive_item_service_1.DriveItemService.instance.getFileByPath(url);
|
|
56
57
|
}
|
|
57
|
-
catch {
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (errors_utils_1.ErrorUtils.isNotFoundError(error))
|
|
60
|
+
return undefined;
|
|
61
|
+
throw error;
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
static async getDriveFolderFromResource(url) {
|
|
61
65
|
try {
|
|
62
66
|
return await drive_item_service_1.DriveItemService.instance.getFolderByPath(url);
|
|
63
67
|
}
|
|
64
|
-
catch {
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (errors_utils_1.ErrorUtils.isNotFoundError(error))
|
|
70
|
+
return undefined;
|
|
71
|
+
throw error;
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
static async getDriveItemFromResource(resource) {
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
if (resource.url.endsWith('/')) {
|
|
76
|
+
return await this.getDriveFolderFromResource(resource.url);
|
|
77
|
+
}
|
|
78
|
+
const lookupFile = () => this.getDriveFileFromResource(resource.url);
|
|
79
|
+
const lookupFolder = () => this.getDriveFolderFromResource(resource.url);
|
|
80
|
+
const cached = await drive_item_repository_1.DriveItemRepository.instance.getByPath(resource.url);
|
|
81
|
+
const [first, second] = cached?.type === 'folder' ? [lookupFolder, lookupFile] : [lookupFile, lookupFolder];
|
|
82
|
+
let inconclusive;
|
|
70
83
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else {
|
|
75
|
-
try {
|
|
76
|
-
item = await drive_item_service_1.DriveItemService.instance.getFileByPath(resource.url);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
item = await drive_item_service_1.DriveItemService.instance.getFolderByPath(resource.url);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
84
|
+
const item = await first();
|
|
85
|
+
if (item)
|
|
86
|
+
return item;
|
|
82
87
|
}
|
|
83
|
-
catch {
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!(error instanceof errors_utils_1.ServiceUnavailableError))
|
|
90
|
+
throw error;
|
|
91
|
+
inconclusive = error;
|
|
84
92
|
}
|
|
85
|
-
|
|
93
|
+
const item = await second();
|
|
94
|
+
if (item)
|
|
95
|
+
return item;
|
|
96
|
+
if (inconclusive)
|
|
97
|
+
throw inconclusive;
|
|
98
|
+
return undefined;
|
|
86
99
|
}
|
|
87
100
|
static async deleteOrTrashItem(driveItem) {
|
|
88
101
|
const configs = await config_service_1.ConfigService.instance.readWebdavConfig();
|