@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.
Files changed (48) hide show
  1. package/README.md +28 -28
  2. package/bin/dev.js +10 -0
  3. package/bin/run.js +10 -0
  4. package/dist/commands/list.d.ts +2 -0
  5. package/dist/commands/move-file.d.ts +2 -0
  6. package/dist/commands/trash-list.d.ts +1 -2
  7. package/dist/commands/trash-restore-file.d.ts +2 -0
  8. package/dist/commands/upload-file.d.ts +2 -2
  9. package/dist/services/database/database.service.d.ts +6 -0
  10. package/dist/services/database/database.service.js +27 -0
  11. package/dist/services/database/drive-item/drive-item.repository.d.ts +2 -1
  12. package/dist/services/database/drive-item/drive-item.repository.js +10 -5
  13. package/dist/services/drive/drive-file.service.js +4 -6
  14. package/dist/services/drive/drive-folder.service.d.ts +9 -5
  15. package/dist/services/drive/drive-folder.service.js +48 -31
  16. package/dist/services/drive/drive-item.service.d.ts +1 -0
  17. package/dist/services/drive/drive-item.service.js +24 -14
  18. package/dist/services/drive/trash.service.d.ts +5 -4
  19. package/dist/services/drive/trash.service.js +21 -22
  20. package/dist/services/webdav/webdav-folder.service.d.ts +4 -0
  21. package/dist/services/webdav/webdav-folder.service.js +14 -1
  22. package/dist/types/config.types.d.ts +1 -0
  23. package/dist/types/drive.types.d.ts +1 -0
  24. package/dist/utils/cli.utils.js +2 -8
  25. package/dist/utils/drive.utils.d.ts +1 -0
  26. package/dist/utils/drive.utils.js +9 -1
  27. package/dist/utils/errors.utils.d.ts +35 -0
  28. package/dist/utils/errors.utils.js +116 -3
  29. package/dist/utils/network.utils.js +7 -6
  30. package/dist/utils/pagination.utils.d.ts +7 -0
  31. package/dist/utils/pagination.utils.js +20 -0
  32. package/dist/utils/webdav.utils.js +30 -17
  33. package/dist/webdav/handlers/DELETE.handler.js +0 -2
  34. package/dist/webdav/handlers/GET.handler.js +15 -5
  35. package/dist/webdav/handlers/HEAD.handler.js +17 -7
  36. package/dist/webdav/handlers/LOCK.handler.js +5 -1
  37. package/dist/webdav/handlers/MKCOL.handler.js +7 -1
  38. package/dist/webdav/handlers/MOVE.handler.js +13 -1
  39. package/dist/webdav/handlers/OPTIONS.handler.js +3 -3
  40. package/dist/webdav/handlers/PROPFIND.handler.js +2 -2
  41. package/dist/webdav/handlers/PROPPATCH.handler.d.ts +3 -1
  42. package/dist/webdav/handlers/PROPPATCH.handler.js +53 -3
  43. package/dist/webdav/handlers/PUT.handler.js +2 -7
  44. package/dist/webdav/index.js +3 -2
  45. package/dist/webdav/middewares/auth.middleware.js +3 -2
  46. package/dist/webdav/middewares/errors.middleware.js +10 -14
  47. package/oclif.manifest.json +1 -1
  48. 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.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);
@@ -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}\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) {
@@ -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 Error(`Multi Range-Requests functionality is not implemented. ${JSON.stringify(rangeOptions)}`);
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 Error(`Empty Range-Request. ${JSON.stringify(rangeOptions)}`);
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 Error(`Unkwnown Range-Request type "${parsed.type}". ${JSON.stringify(rangeOptions)}`);
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 Error(`Unsatisfiable Range-Request. ${JSON.stringify(rangeOptions)}`);
104
+ throw new errors_utils_1.RangeNotSatisfiableError(`Unsatisfiable Range-Request. ${JSON.stringify(rangeOptions)}`);
104
105
  }
105
106
  else if (parsed === -2) {
106
- throw new Error(`Malformed Range-Request. ${JSON.stringify(rangeOptions)}`);
107
+ throw new errors_utils_1.BadRequestError(`Malformed Range-Request. ${JSON.stringify(rangeOptions)}`);
107
108
  }
108
109
  else {
109
- throw new Error(`Unknown error from Range-Request. ${JSON.stringify(rangeOptions)}`);
110
+ throw new errors_utils_1.BadRequestError(`Unknown error from Range-Request. ${JSON.stringify(rangeOptions)}`);
110
111
  }
111
112
  }
112
113
  }
@@ -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();