@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.
Files changed (36) hide show
  1. package/README.md +28 -28
  2. package/dist/commands/list.d.ts +2 -0
  3. package/dist/commands/move-file.d.ts +2 -0
  4. package/dist/commands/trash-list.d.ts +1 -2
  5. package/dist/commands/trash-restore-file.d.ts +2 -0
  6. package/dist/commands/upload-file.d.ts +2 -2
  7. package/dist/services/database/database.service.d.ts +6 -0
  8. package/dist/services/database/database.service.js +27 -0
  9. package/dist/services/database/drive-item/drive-item.repository.d.ts +2 -1
  10. package/dist/services/database/drive-item/drive-item.repository.js +10 -5
  11. package/dist/services/drive/drive-file.service.js +4 -6
  12. package/dist/services/drive/drive-folder.service.d.ts +9 -5
  13. package/dist/services/drive/drive-folder.service.js +44 -29
  14. package/dist/services/drive/drive-item.service.d.ts +1 -0
  15. package/dist/services/drive/drive-item.service.js +24 -14
  16. package/dist/services/drive/trash.service.d.ts +5 -4
  17. package/dist/services/drive/trash.service.js +21 -22
  18. package/dist/services/webdav/webdav-folder.service.d.ts +4 -0
  19. package/dist/services/webdav/webdav-folder.service.js +14 -1
  20. package/dist/types/config.types.d.ts +1 -0
  21. package/dist/types/drive.types.d.ts +1 -0
  22. package/dist/utils/cli.utils.js +2 -8
  23. package/dist/utils/drive.utils.d.ts +1 -0
  24. package/dist/utils/drive.utils.js +9 -1
  25. package/dist/utils/errors.utils.d.ts +27 -0
  26. package/dist/utils/errors.utils.js +98 -3
  27. package/dist/utils/pagination.utils.d.ts +7 -0
  28. package/dist/utils/pagination.utils.js +20 -0
  29. package/dist/utils/webdav.utils.js +30 -17
  30. package/dist/webdav/handlers/MKCOL.handler.js +7 -1
  31. package/dist/webdav/handlers/PUT.handler.js +1 -1
  32. package/dist/webdav/index.js +3 -2
  33. package/dist/webdav/middewares/auth.middleware.js +3 -2
  34. package/dist/webdav/middewares/errors.middleware.js +7 -13
  35. package/oclif.manifest.json +1 -1
  36. 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.logger.warn('File metadata by uuid failed, falling back to path lookup', { path, uuid: cached.uuid });
27
- await drive_item_repository_1.DriveItemRepository.instance.delete([cached.uuid]);
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.logger.warn('Folder metadata by uuid failed, falling back to path lookup', { path, uuid: cached.uuid });
46
- await drive_item_repository_1.DriveItemRepository.instance.delete([cached.uuid]);
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
- throw new errors_utils_1.NotFoundError(`File not found at path: ${path}`);
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
- throw new errors_utils_1.NotFoundError(`Folder not found at path: ${path}`);
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
- isFavorite?: boolean;
54
+ sharings?: import("@internxt/sdk/dist/schema").components["schemas"]["FileSharingDto"][];
55
55
  }[];
56
56
  }>;
57
- private readonly getAllTrashSubfolders;
58
- private readonly getAllTrashSubfiles;
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.getAllTrashSubfolders(0);
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
- getAllTrashSubfolders = async (offset) => {
37
- const trashClient = sdk_manager_service_1.SdkManager.instance.getTrash();
38
- const promise = trashClient.getTrashedFilesPaginated(50, offset, 'folders', true);
39
- const folders = (await promise).result;
40
- if (folders.length > 0) {
41
- return folders.concat(await this.getAllTrashSubfolders(offset + folders.length));
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
- getAllTrashSubfiles = async (offset) => {
48
- const trashClient = sdk_manager_service_1.SdkManager.instance.getTrash();
49
- const promise = trashClient.getTrashedFilesPaginated(50, offset, 'files', true);
50
- const files = (await promise).result;
51
- if (files.length > 0) {
52
- return files.concat(await this.getAllTrashSubfiles(offset + files.length));
53
- }
54
- else {
55
- return files;
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.createFolder({ folderName: currentFolderName, parentFolderUuid }));
54
+ (await this.createOrReuseFolder({ folderName: currentFolderName, parentFolderUuid }));
42
55
  if (rest.length === 0) {
43
56
  return folder;
44
57
  }
@@ -7,4 +7,5 @@ export interface ConfigKeys {
7
7
  readonly APP_MAGIC_SALT: string;
8
8
  readonly NETWORK_URL: string;
9
9
  readonly DESKTOP_HEADER: string;
10
+ readonly INXT_SQLITE_JOURNAL_MODE: string;
10
11
  }
@@ -1,4 +1,5 @@
1
1
  import { FileMeta, FolderMeta } from '@internxt/sdk/dist/drive/storage/types';
2
+ export type DriveItemType = 'file' | 'folder';
2
3
  export type DriveFileItem = Pick<FileMeta, 'uuid' | 'name' | 'bucket' | 'folderUuid' | 'status'> & {
3
4
  itemType: 'file';
4
5
  size: number;
@@ -185,13 +185,7 @@ class CLIUtils {
185
185
  };
186
186
  static catchError = ({ error, logReporter, command, jsonFlag, debugMode, }) => {
187
187
  let message = '';
188
- let requestId;
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 + (requestId ? ` (requestId: ${requestId})` : ''));
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}\nProperties => ${JSON.stringify(props, null, 2)}\nStack => ${error.stack}`);
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)}\nProperties => ${JSON.stringify(props, null, 2)}\n`);
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,7 @@
1
+ export declare class PaginationUtils {
2
+ static readonly MAX_PAGE_SIZE = 1000;
3
+ static readonly fetchAllPages: <T>(fetchPage: (cursor?: string) => Promise<{
4
+ items: T[];
5
+ nextCursor: string | null;
6
+ }>) => Promise<T[]>;
7
+ }
@@ -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
- let item = undefined;
69
- const isFolder = resource.url.endsWith('/');
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
- if (isFolder) {
72
- item = await drive_item_service_1.DriveItemService.instance.getFolderByPath(resource.url);
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
- return item;
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.createFolder({
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
  }
@@ -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: ${message}\nStack: ${error.stack}`);
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: ${message}`);
33
+ logger_utils_1.webdavLogger.error(`Error from AuthMiddleware: ${logMessage}`);
33
34
  }
34
35
  }
35
36
  const errorBodyXML = xml_utils_1.XMLUtils.toWebDavXML({