@internxt/cli 1.6.9 → 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/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 +44 -29
- 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 +27 -0
- package/dist/utils/errors.utils.js +98 -3
- 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/MKCOL.handler.js +7 -1
- package/dist/webdav/handlers/PUT.handler.js +1 -1
- package/dist/webdav/index.js +3 -2
- package/dist/webdav/middewares/auth.middleware.js +3 -2
- package/dist/webdav/middewares/errors.middleware.js +7 -13
- package/oclif.manifest.json +1 -1
- package/package.json +18 -13
|
@@ -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);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RangeNotSatisfiableError = exports.PreconditionFailedError = 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) {
|
|
@@ -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();
|
|
@@ -20,9 +20,15 @@ class MKCOLRequestHandler {
|
|
|
20
20
|
logger_utils_1.webdavLogger.info(`[MKCOL] Folder '${resource.url}' already exists, throwing 405 error`);
|
|
21
21
|
throw new errors_utils_1.MethodNotAllowed('Folder already exists');
|
|
22
22
|
}
|
|
23
|
-
const newFolder = await webdav_folder_service_1.WebDavFolderService.instance
|
|
23
|
+
const newFolder = await webdav_folder_service_1.WebDavFolderService.instance
|
|
24
|
+
.createFolder({
|
|
24
25
|
folderName: resource.path.base,
|
|
25
26
|
parentFolderUuid: parentDriveFolderItem.uuid,
|
|
27
|
+
})
|
|
28
|
+
.catch((error) => {
|
|
29
|
+
if (errors_utils_1.ErrorUtils.isAlreadyExistsError(error))
|
|
30
|
+
throw new errors_utils_1.MethodNotAllowed('Folder already exists');
|
|
31
|
+
throw error;
|
|
26
32
|
});
|
|
27
33
|
logger_utils_1.webdavLogger.info(`[MKCOL] ✅ Folder created with UUID ${newFolder.uuid}`);
|
|
28
34
|
await drive_item_repository_1.DriveItemRepository.instance.createOrUpdate([
|
|
@@ -88,7 +88,7 @@ class PUTRequestHandler {
|
|
|
88
88
|
file = await drive_file_service_1.DriveFileService.instance.replaceFile(driveFileItem.uuid, filePayload);
|
|
89
89
|
}
|
|
90
90
|
catch (error) {
|
|
91
|
-
logger_utils_1.webdavLogger.warn(`[PUT] File replace failed for '${resource.url}', falling back to delete and create: ${error instanceof Error ? error.message : String(error)}
|
|
91
|
+
logger_utils_1.webdavLogger.warn(errors_utils_1.ErrorUtils.withRequestId(`[PUT] File replace failed for '${resource.url}', falling back to delete and create: ${error instanceof Error ? error.message : String(error)}`, error));
|
|
92
92
|
await webdav_utils_1.WebDavUtils.deleteOrTrashItem(driveFileItem);
|
|
93
93
|
file = await drive_file_service_1.DriveFileService.instance.createFile(filePayload);
|
|
94
94
|
}
|
package/dist/webdav/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const auth_service_1 = require("../services/auth.service");
|
|
|
11
11
|
const logger_utils_1 = require("../utils/logger.utils");
|
|
12
12
|
const sdk_manager_service_1 = require("../services/sdk-manager.service");
|
|
13
13
|
const database_service_1 = require("../services/database/database.service");
|
|
14
|
+
const errors_utils_1 = require("../utils/errors.utils");
|
|
14
15
|
dotenv_1.default.config({ quiet: true });
|
|
15
16
|
const init = async () => {
|
|
16
17
|
await config_service_1.ConfigService.instance.ensureInternxtCliDataDirExists();
|
|
@@ -23,9 +24,9 @@ const init = async () => {
|
|
|
23
24
|
new webdav_server_1.WebDavServer((0, express_1.default)())
|
|
24
25
|
.start()
|
|
25
26
|
.then()
|
|
26
|
-
.catch((err) => logger_utils_1.webdavLogger.error('Failed to start WebDAV server', err));
|
|
27
|
+
.catch((err) => logger_utils_1.webdavLogger.error(errors_utils_1.ErrorUtils.withRequestId('Failed to start WebDAV server', err), err));
|
|
27
28
|
};
|
|
28
29
|
process.on('uncaughtException', (err) => {
|
|
29
|
-
logger_utils_1.webdavLogger.error('Unhandled exception:', err);
|
|
30
|
+
logger_utils_1.webdavLogger.error(errors_utils_1.ErrorUtils.withRequestId('Unhandled exception:', err), err);
|
|
30
31
|
});
|
|
31
32
|
init();
|
|
@@ -25,11 +25,12 @@ const AuthMiddleware = () => {
|
|
|
25
25
|
let message = 'Authentication required to access this resource.';
|
|
26
26
|
if (errors_utils_1.ErrorUtils.isError(error)) {
|
|
27
27
|
message = error.message;
|
|
28
|
+
const logMessage = errors_utils_1.ErrorUtils.withRequestId(message, error);
|
|
28
29
|
if (error.stack) {
|
|
29
|
-
logger_utils_1.webdavLogger.error(`Error from AuthMiddleware: ${
|
|
30
|
+
logger_utils_1.webdavLogger.error(`Error from AuthMiddleware: ${logMessage}\nStack: ${error.stack}`);
|
|
30
31
|
}
|
|
31
32
|
else {
|
|
32
|
-
logger_utils_1.webdavLogger.error(`Error from AuthMiddleware: ${
|
|
33
|
+
logger_utils_1.webdavLogger.error(`Error from AuthMiddleware: ${logMessage}`);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
const errorBodyXML = xml_utils_1.XMLUtils.toWebDavXML({
|