@internxt/cli 1.6.7 → 1.6.9

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 (87) hide show
  1. package/README.md +36 -231
  2. package/bin/dev.js +10 -0
  3. package/bin/run.js +10 -0
  4. package/dist/commands/create-folder.js +2 -6
  5. package/dist/commands/delete-permanently-file.js +1 -1
  6. package/dist/commands/delete-permanently-folder.js +1 -1
  7. package/dist/commands/download-file.js +3 -3
  8. package/dist/commands/list.d.ts +2 -0
  9. package/dist/commands/list.js +1 -1
  10. package/dist/commands/login.d.ts +0 -1
  11. package/dist/commands/login.js +1 -11
  12. package/dist/commands/move-file.d.ts +1 -0
  13. package/dist/commands/move-file.js +1 -1
  14. package/dist/commands/move-folder.js +1 -1
  15. package/dist/commands/rename-file.js +1 -1
  16. package/dist/commands/rename-folder.js +1 -1
  17. package/dist/commands/trash-file.js +1 -1
  18. package/dist/commands/trash-folder.js +1 -1
  19. package/dist/commands/trash-list.d.ts +2 -0
  20. package/dist/commands/trash-restore-file.d.ts +1 -0
  21. package/dist/commands/trash-restore-file.js +1 -1
  22. package/dist/commands/trash-restore-folder.js +1 -1
  23. package/dist/commands/upload-file.d.ts +1 -0
  24. package/dist/commands/upload-file.js +22 -13
  25. package/dist/hooks/prerun/auth_check.js +2 -2
  26. package/dist/services/auth.service.d.ts +3 -3
  27. package/dist/services/auth.service.js +30 -40
  28. package/dist/services/crypto.service.d.ts +0 -4
  29. package/dist/services/crypto.service.js +1 -32
  30. package/dist/services/database/drive-item/drive-item.model.js +1 -0
  31. package/dist/services/database/drive-item/drive-item.repository.js +12 -2
  32. package/dist/services/drive/drive-file.service.d.ts +2 -0
  33. package/dist/services/drive/drive-file.service.js +48 -18
  34. package/dist/services/drive/drive-folder.service.d.ts +2 -0
  35. package/dist/services/drive/drive-folder.service.js +18 -48
  36. package/dist/services/drive/trash.service.d.ts +2 -0
  37. package/dist/services/drive/trash.service.js +14 -41
  38. package/dist/services/network/upload/upload-file.service.js +5 -9
  39. package/dist/services/network/upload/upload.types.d.ts +2 -1
  40. package/dist/services/sdk-manager.service.d.ts +0 -2
  41. package/dist/services/sdk-manager.service.js +0 -7
  42. package/dist/services/thumbnail.service.d.ts +4 -13
  43. package/dist/services/thumbnail.service.js +7 -21
  44. package/dist/services/universal-link.service.d.ts +1 -1
  45. package/dist/services/universal-link.service.js +2 -2
  46. package/dist/services/validation.service.d.ts +1 -1
  47. package/dist/services/validation.service.js +3 -2
  48. package/dist/types/command.types.d.ts +0 -29
  49. package/dist/types/command.types.js +1 -15
  50. package/dist/types/network.types.d.ts +3 -11
  51. package/dist/types/webdav.types.d.ts +0 -3
  52. package/dist/utils/cli.utils.js +9 -22
  53. package/dist/utils/errors.utils.d.ts +8 -0
  54. package/dist/utils/errors.utils.js +19 -1
  55. package/dist/utils/logger.utils.js +9 -2
  56. package/dist/utils/network.utils.js +7 -6
  57. package/dist/utils/stream.utils.d.ts +2 -2
  58. package/dist/utils/stream.utils.js +7 -6
  59. package/dist/utils/thumbnail.utils.d.ts +1 -3
  60. package/dist/utils/thumbnail.utils.js +3 -13
  61. package/dist/utils/upload.utils.d.ts +1 -1
  62. package/dist/utils/upload.utils.js +2 -2
  63. package/dist/utils/webdav.utils.d.ts +2 -0
  64. package/dist/utils/webdav.utils.js +16 -0
  65. package/dist/webdav/handlers/DELETE.handler.js +0 -2
  66. package/dist/webdav/handlers/GET.handler.js +18 -7
  67. package/dist/webdav/handlers/HEAD.handler.js +18 -7
  68. package/dist/webdav/handlers/LOCK.handler.js +5 -1
  69. package/dist/webdav/handlers/MOVE.handler.js +13 -1
  70. package/dist/webdav/handlers/OPTIONS.handler.js +3 -3
  71. package/dist/webdav/handlers/PROPFIND.handler.js +5 -6
  72. package/dist/webdav/handlers/PROPPATCH.handler.d.ts +3 -1
  73. package/dist/webdav/handlers/PROPPATCH.handler.js +53 -3
  74. package/dist/webdav/handlers/PUT.handler.js +19 -10
  75. package/dist/webdav/index.js +2 -2
  76. package/dist/webdav/middewares/auth.middleware.js +1 -1
  77. package/dist/webdav/middewares/errors.middleware.js +32 -6
  78. package/oclif.manifest.json +9 -198
  79. package/package.json +26 -25
  80. package/dist/commands/workspaces-list.d.ts +0 -20
  81. package/dist/commands/workspaces-list.js +0 -68
  82. package/dist/commands/workspaces-unset.d.ts +0 -23
  83. package/dist/commands/workspaces-unset.js +0 -46
  84. package/dist/commands/workspaces-use.d.ts +0 -27
  85. package/dist/commands/workspaces-use.js +0 -114
  86. package/dist/services/drive/workspace.service.d.ts +0 -7
  87. package/dist/services/drive/workspace.service.js +0 -30
@@ -1,6 +1,6 @@
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.NotFoundError = exports.ConflictError = exports.ErrorUtils = void 0;
4
4
  const logger_utils_1 = require("./logger.utils");
5
5
  class ErrorUtils {
6
6
  static isError = (error) => {
@@ -80,3 +80,21 @@ class NotImplementedError extends Error {
80
80
  }
81
81
  }
82
82
  exports.NotImplementedError = NotImplementedError;
83
+ class PreconditionFailedError extends Error {
84
+ statusCode = 412;
85
+ constructor(message) {
86
+ super(message);
87
+ this.name = 'PreconditionFailedError';
88
+ Object.setPrototypeOf(this, PreconditionFailedError.prototype);
89
+ }
90
+ }
91
+ exports.PreconditionFailedError = PreconditionFailedError;
92
+ class RangeNotSatisfiableError extends Error {
93
+ statusCode = 416;
94
+ constructor(message) {
95
+ super(message);
96
+ this.name = 'RangeNotSatisfiableError';
97
+ Object.setPrototypeOf(this, RangeNotSatisfiableError.prototype);
98
+ }
99
+ }
100
+ exports.RangeNotSatisfiableError = RangeNotSatisfiableError;
@@ -4,14 +4,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.webdavLogger = exports.logger = void 0;
7
+ const node_os_1 = __importDefault(require("node:os"));
7
8
  const winston_1 = __importDefault(require("winston"));
8
9
  const configs_1 = require("../constants/configs");
10
+ const package_json_1 = __importDefault(require("../../package.json"));
9
11
  const maxLogSize = 40 * 1024 * 1024;
10
12
  const maxLogsFiles = 5;
13
+ const environmentMeta = {
14
+ version: package_json_1.default.version,
15
+ os: `${node_os_1.default.platform()} ${node_os_1.default.release()} (${node_os_1.default.arch()})`,
16
+ node: process.version,
17
+ };
11
18
  exports.logger = winston_1.default.createLogger({
12
19
  level: 'info',
13
20
  format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json()),
14
- defaultMeta: { service: 'internxt-cli' },
21
+ defaultMeta: { service: 'internxt-cli', ...environmentMeta },
15
22
  transports: [
16
23
  new winston_1.default.transports.File({
17
24
  filename: 'internxt-cli-error.log',
@@ -33,7 +40,7 @@ exports.logger = winston_1.default.createLogger({
33
40
  exports.webdavLogger = winston_1.default.createLogger({
34
41
  level: 'info',
35
42
  format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json()),
36
- defaultMeta: { service: 'internxt-webdav' },
43
+ defaultMeta: { service: 'internxt-webdav', ...environmentMeta },
37
44
  transports: [
38
45
  new winston_1.default.transports.File({
39
46
  filename: 'internxt-webdav-error.log',
@@ -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
  }
@@ -6,10 +6,10 @@ export declare class StreamUtils {
6
6
  static joinReadableBinaryStreams(streams: ReadableStream<Uint8Array>[]): ReadableStream<Uint8Array>;
7
7
  }
8
8
  export declare class BufferStream extends Transform {
9
- buffer: Buffer | null;
9
+ private chunks;
10
10
  constructor(opts?: TransformOptions);
11
11
  _transform(chunk: Buffer, _: BufferEncoding, callback: TransformCallback): void;
12
12
  _flush(callback: TransformCallback): void;
13
13
  reset(): void;
14
- getBuffer(): Buffer | null;
14
+ getBuffer(): Buffer | undefined;
15
15
  }
@@ -56,24 +56,25 @@ class StreamUtils {
56
56
  }
57
57
  exports.StreamUtils = StreamUtils;
58
58
  class BufferStream extends node_stream_1.Transform {
59
- buffer;
59
+ chunks;
60
60
  constructor(opts) {
61
61
  super(opts);
62
- this.buffer = null;
62
+ this.chunks = [];
63
63
  }
64
64
  _transform(chunk, _, callback) {
65
- const currentBuffer = this.buffer ?? Buffer.alloc(0);
66
- this.buffer = Buffer.concat([currentBuffer, chunk]);
65
+ this.chunks.push(chunk);
67
66
  callback(null, chunk);
68
67
  }
69
68
  _flush(callback) {
70
69
  callback();
71
70
  }
72
71
  reset() {
73
- this.buffer = null;
72
+ this.chunks = [];
74
73
  }
75
74
  getBuffer() {
76
- return this.buffer;
75
+ if (this.chunks.length === 0)
76
+ return undefined;
77
+ return Buffer.concat(this.chunks);
77
78
  }
78
79
  }
79
80
  exports.BufferStream = BufferStream;
@@ -5,8 +5,6 @@ export declare const ThumbnailConfig: {
5
5
  readonly Type: "png";
6
6
  };
7
7
  export declare class ThumbnailUtils {
8
- static readonly MAX_IMAGE_THUMBNAILABLE_SIZE_IN_MB: number;
9
- static readonly isFileThumbnailable: (fileType: string) => boolean;
10
- static readonly isPDFThumbnailable: (fileType: string) => boolean;
8
+ static readonly MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES: number;
11
9
  static readonly isImageThumbnailable: (fileType: string, size: number) => boolean;
12
10
  }
@@ -18,9 +18,6 @@ const imageExtensions = {
18
18
  raw: ['raw', 'cr2', 'nef', 'orf', 'sr2'],
19
19
  webp: ['webp'],
20
20
  };
21
- const pdfExtensions = {
22
- pdf: ['pdf'],
23
- };
24
21
  const thumbnailableImageExtension = new Set([
25
22
  ...imageExtensions['jpg'],
26
23
  ...imageExtensions['png'],
@@ -28,19 +25,12 @@ const thumbnailableImageExtension = new Set([
28
25
  ...imageExtensions['gif'],
29
26
  ...imageExtensions['tiff'],
30
27
  ]);
31
- const thumbnailablePdfExtension = new Set(pdfExtensions['pdf']);
32
- const thumbnailableExtension = new Set(thumbnailableImageExtension);
33
28
  class ThumbnailUtils {
34
- static MAX_IMAGE_THUMBNAILABLE_SIZE_IN_MB = 500 * 1024 * 1024;
35
- static isFileThumbnailable = (fileType) => {
36
- return fileType.trim().length > 0 && thumbnailableExtension.has(fileType.trim().toLowerCase());
37
- };
38
- static isPDFThumbnailable = (fileType) => {
39
- return fileType.trim().length > 0 && thumbnailablePdfExtension.has(fileType.trim().toLowerCase());
40
- };
29
+ static MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES = 128 * 1024 * 1024;
41
30
  static isImageThumbnailable = (fileType, size) => {
42
- if (size > ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_MB)
31
+ if (size <= 0 || size > ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES) {
43
32
  return false;
33
+ }
44
34
  return fileType.trim().length > 0 && thumbnailableImageExtension.has(fileType.trim().toLowerCase());
45
35
  };
46
36
  }
@@ -2,7 +2,7 @@ import { Readable } from 'node:stream';
2
2
  import { BufferStream } from './stream.utils';
3
3
  export declare class UploadUtils {
4
4
  static readonly checkUploadSizeLimits: (size: number) => Promise<void>;
5
- static readonly prepareUploadStreams: (readable: Readable, fileType: string) => {
5
+ static readonly prepareUploadStreams: (readable: Readable, fileType: string, size: number) => {
6
6
  fileStream: Readable;
7
7
  thumbnailStream: BufferStream | undefined;
8
8
  isThumbnailable: boolean;
@@ -19,8 +19,8 @@ class UploadUtils {
19
19
  throw new Error('File is too big (more than 100 GB)');
20
20
  }
21
21
  };
22
- static prepareUploadStreams = (readable, fileType) => {
23
- const isThumbnailable = thumbnail_utils_1.ThumbnailUtils.isFileThumbnailable(fileType);
22
+ static prepareUploadStreams = (readable, fileType, size) => {
23
+ const isThumbnailable = thumbnail_utils_1.ThumbnailUtils.isImageThumbnailable(fileType, size);
24
24
  if (!isThumbnailable) {
25
25
  return { fileStream: readable, thumbnailStream: undefined, isThumbnailable };
26
26
  }
@@ -13,4 +13,6 @@ export declare class WebDavUtils {
13
13
  itemType: 'file' | 'folder';
14
14
  uuid: string;
15
15
  }>(driveItem: T): Promise<void>;
16
+ static generateETag(parts: Array<string | number | null | undefined>): string;
17
+ static getItemETag(driveItem: DriveFileItem | DriveFolderItem): string;
16
18
  }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.WebDavUtils = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
+ const node_crypto_1 = require("node:crypto");
8
9
  const drive_item_service_1 = require("../services/drive/drive-item.service");
9
10
  const logger_utils_1 = require("./logger.utils");
10
11
  const config_service_1 = require("../services/config.service");
@@ -100,5 +101,20 @@ class WebDavUtils {
100
101
  }
101
102
  await drive_item_repository_1.DriveItemRepository.instance.delete([driveItem.uuid]);
102
103
  }
104
+ static generateETag(parts) {
105
+ const normalized = parts.map((part) => part ?? '-');
106
+ const hash = (0, node_crypto_1.createHash)('sha256').update(normalized.join('|')).digest('hex');
107
+ return `"${hash}"`;
108
+ }
109
+ static getItemETag(driveItem) {
110
+ return this.generateETag([
111
+ driveItem.uuid,
112
+ driveItem.itemType === 'file' ? driveItem.size : undefined,
113
+ driveItem.createdAt.getTime(),
114
+ driveItem.updatedAt.getTime(),
115
+ driveItem.creationTime.getTime(),
116
+ driveItem.modificationTime.getTime(),
117
+ ]);
118
+ }
103
119
  }
104
120
  exports.WebDavUtils = WebDavUtils;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DELETERequestHandler = void 0;
4
- const drive_item_repository_1 = require("../../services/database/drive-item/drive-item.repository");
5
4
  const webdav_utils_1 = require("../../utils/webdav.utils");
6
5
  const logger_utils_1 = require("../../utils/logger.utils");
7
6
  const errors_utils_1 = require("../../utils/errors.utils");
@@ -14,7 +13,6 @@ class DELETERequestHandler {
14
13
  throw new errors_utils_1.NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
15
14
  }
16
15
  await webdav_utils_1.WebDavUtils.deleteOrTrashItem(driveItem);
17
- await drive_item_repository_1.DriveItemRepository.instance.delete([driveItem.uuid]);
18
16
  res.status(204).send();
19
17
  };
20
18
  }
@@ -21,17 +21,28 @@ class GETRequestHandler {
21
21
  logger_utils_1.webdavLogger.info(`[GET] [${driveFile.uuid}] Network ready for download`);
22
22
  res.header('Content-Type', 'application/octet-stream');
23
23
  res.header('Accept-Ranges', 'bytes');
24
+ res.header('ETag', webdav_utils_1.WebDavUtils.getItemETag(driveFile));
24
25
  const fileSize = driveFile.size ?? 0;
25
26
  if (fileSize > 0) {
26
27
  const range = req.headers['range'];
27
- const rangeOptions = network_utils_1.NetworkUtils.parseRangeHeader({
28
- range,
29
- totalFileSize: fileSize,
30
- });
28
+ let rangeOptions;
29
+ try {
30
+ rangeOptions = network_utils_1.NetworkUtils.parseRangeHeader({
31
+ range,
32
+ totalFileSize: fileSize,
33
+ });
34
+ }
35
+ catch (error) {
36
+ if (error instanceof errors_utils_1.RangeNotSatisfiableError) {
37
+ res.header('Content-Range', `bytes */${fileSize}`);
38
+ }
39
+ throw error;
40
+ }
31
41
  let contentLength = fileSize;
32
42
  if (rangeOptions) {
33
43
  logger_utils_1.webdavLogger.info(`[GET] [${driveFile.uuid}] Range request received:`, { rangeOptions });
34
44
  contentLength = rangeOptions.rangeSize;
45
+ res.header('Content-Range', `bytes ${rangeOptions.parsed.start}-${rangeOptions.parsed.end}/${fileSize}`);
35
46
  }
36
47
  res.header('Content-length', contentLength.toString());
37
48
  const writable = new WritableStream({
@@ -45,10 +56,10 @@ class GETRequestHandler {
45
56
  if (!driveFile.fileId) {
46
57
  throw new command_types_1.NotValidFileIdError();
47
58
  }
48
- const { networkFacade, bucket, mnemonic } = await cli_utils_1.CLIUtils.prepareNetwork(user);
49
- const [executeDownload] = await networkFacade.downloadToStream(bucket, mnemonic, driveFile.fileId, contentLength, writable, rangeOptions);
59
+ const { networkFacade, mnemonic } = await cli_utils_1.CLIUtils.prepareNetwork(user);
60
+ const [executeDownload] = await networkFacade.downloadToStream(driveFile.bucket, mnemonic, driveFile.fileId, contentLength, writable, rangeOptions);
50
61
  logger_utils_1.webdavLogger.info(`[GET] [${driveFile.uuid}] Download prepared, executing...`);
51
- res.status(200);
62
+ res.status(rangeOptions ? 206 : 200);
52
63
  await executeDownload;
53
64
  logger_utils_1.webdavLogger.info(`[GET] [${driveFile.uuid}] ✅ Download ready, replying to client`);
54
65
  }
@@ -14,22 +14,33 @@ class HEADRequestHandler {
14
14
  throw new errors_utils_1.NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
15
15
  }
16
16
  logger_utils_1.webdavLogger.info(`[HEAD] [${driveItem.uuid}] Found Drive item`);
17
+ res.header('ETag', webdav_utils_1.WebDavUtils.getItemETag(driveItem));
18
+ let rangeOptions;
17
19
  if (driveItem.itemType === 'file') {
18
20
  const range = req.headers['range'];
19
- const rangeOptions = network_utils_1.NetworkUtils.parseRangeHeader({
20
- range,
21
- totalFileSize: driveItem.size,
22
- });
21
+ try {
22
+ rangeOptions = network_utils_1.NetworkUtils.parseRangeHeader({
23
+ range,
24
+ totalFileSize: driveItem.size,
25
+ });
26
+ }
27
+ catch (error) {
28
+ if (error instanceof errors_utils_1.RangeNotSatisfiableError) {
29
+ res.header('Content-Range', `bytes */${driveItem.size}`);
30
+ }
31
+ throw error;
32
+ }
23
33
  let contentLength = driveItem.size;
34
+ res.header('Content-Type', 'application/octet-stream');
35
+ res.header('Accept-Ranges', 'bytes');
24
36
  if (rangeOptions) {
25
37
  logger_utils_1.webdavLogger.info(`[HEAD] [${driveItem.uuid}] Range request received:`, { rangeOptions });
26
38
  contentLength = rangeOptions.rangeSize;
39
+ res.header('Content-Range', `bytes ${rangeOptions.parsed.start}-${rangeOptions.parsed.end}/${driveItem.size}`);
27
40
  }
28
- res.header('Content-Type', 'application/octet-stream');
29
- res.header('Accept-Ranges', 'bytes');
30
41
  res.header('Content-length', contentLength.toString());
31
42
  }
32
- res.status(200).send();
43
+ res.status(rangeOptions ? 206 : 200).send();
33
44
  };
34
45
  }
35
46
  exports.HEADRequestHandler = HEADRequestHandler;
@@ -4,6 +4,7 @@ exports.LOCKRequestHandler = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const logger_utils_1 = require("../../utils/logger.utils");
6
6
  const xml_utils_1 = require("../../utils/xml.utils");
7
+ const webdav_utils_1 = require("../../utils/webdav.utils");
7
8
  const LOCK_TIMEOUT_SECONDS = 300;
8
9
  class LOCKRequestHandler {
9
10
  handle = async (req, res) => {
@@ -42,9 +43,12 @@ class LOCKRequestHandler {
42
43
  },
43
44
  },
44
45
  }, { suppressEmptyNode: true }, 'prop');
46
+ const resource = await webdav_utils_1.WebDavUtils.getRequestedResource(req.url);
47
+ const driveItem = await webdav_utils_1.WebDavUtils.getDriveItemFromResource(resource);
48
+ const statusCode = driveItem ? 200 : 201;
45
49
  res.set('Lock-Token', `<${lockToken}>`);
46
50
  res.set('Content-Type', 'application/xml; charset="utf-8"');
47
- res.status(200).send(lockDiscoveryXml);
51
+ res.status(statusCode).send(lockDiscoveryXml);
48
52
  };
49
53
  }
50
54
  exports.LOCKRequestHandler = LOCKRequestHandler;
@@ -23,6 +23,17 @@ class MOVERequestHandler {
23
23
  if (!originalDriveItem) {
24
24
  throw new errors_utils_1.NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
25
25
  }
26
+ let isOverwrite = false;
27
+ const destinationDriveItem = await webdav_utils_1.WebDavUtils.getDriveItemFromResource(destinationResource);
28
+ if (destinationDriveItem && destinationDriveItem.uuid !== originalDriveItem.uuid) {
29
+ const overwrite = (req.header('overwrite') ?? 'T').toUpperCase();
30
+ if (overwrite === 'F') {
31
+ throw new errors_utils_1.PreconditionFailedError(`Destination '${destinationResource.url}' already exists`);
32
+ }
33
+ isOverwrite = true;
34
+ logger_utils_1.webdavLogger.info(`[MOVE] Destination '${destinationResource.url}' already exists, overwriting it`);
35
+ await webdav_utils_1.WebDavUtils.deleteOrTrashItem(destinationDriveItem);
36
+ }
26
37
  if (destinationResource.path.dir === resource.path.dir) {
27
38
  logger_utils_1.webdavLogger.info(`[MOVE] Renaming ${originalDriveItem.itemType}
28
39
  with UUID ${originalDriveItem.uuid} to ${destinationResource.name}`);
@@ -75,7 +86,8 @@ class MOVERequestHandler {
75
86
  updatedAt: new Date(),
76
87
  },
77
88
  ]);
78
- res.status(204).send();
89
+ const statusCode = isOverwrite ? 204 : 201;
90
+ res.status(statusCode).send();
79
91
  };
80
92
  }
81
93
  exports.MOVERequestHandler = MOVERequestHandler;
@@ -8,21 +8,21 @@ class OPTIONSRequestHandler {
8
8
  const resource = await webdav_utils_1.WebDavUtils.getRequestedResource(req.url);
9
9
  logger_utils_1.webdavLogger.info(`[OPTIONS] Request received for item at ${resource.url}`);
10
10
  if (resource.url === '/' || resource.url === '') {
11
- const allowedMethods = 'DELETE, GET, HEAD, LOCK, MKCOL, MOVE, OPTIONS, PROPFIND, PUT, UNLOCK';
11
+ const allowedMethods = 'DELETE, GET, HEAD, LOCK, MKCOL, MOVE, OPTIONS, PROPFIND, PROPPATCH, PUT, UNLOCK';
12
12
  logger_utils_1.webdavLogger.info(`[OPTIONS] Returning Allowed Options: ${allowedMethods}`);
13
13
  res.header('Allow', allowedMethods);
14
14
  res.header('DAV', '1, 2, ordered-collections');
15
15
  res.status(200).send();
16
16
  }
17
17
  else if (resource.url.endsWith('/')) {
18
- const allowedMethods = 'DELETE, HEAD, LOCK, MKCOL, MOVE, OPTIONS, PROPFIND, UNLOCK';
18
+ const allowedMethods = 'DELETE, HEAD, LOCK, MKCOL, MOVE, OPTIONS, PROPFIND, PROPPATCH, UNLOCK';
19
19
  logger_utils_1.webdavLogger.info(`[OPTIONS] Returning Allowed Options: ${allowedMethods}`);
20
20
  res.header('Allow', allowedMethods);
21
21
  res.header('DAV', '1, 2, ordered-collections');
22
22
  res.status(200).send();
23
23
  }
24
24
  else {
25
- const allowedMethods = 'DELETE, GET, HEAD, LOCK, MOVE, OPTIONS, PROPFIND, PUT, UNLOCK';
25
+ const allowedMethods = 'DELETE, GET, HEAD, LOCK, MOVE, OPTIONS, PROPFIND, PROPPATCH, PUT, UNLOCK';
26
26
  logger_utils_1.webdavLogger.info(`[OPTIONS] Returning Allowed Options: ${allowedMethods}`);
27
27
  res.header('Allow', allowedMethods);
28
28
  res.header('DAV', '1, 2, ordered-collections');
@@ -10,19 +10,18 @@ const drive_item_repository_1 = require("../../services/database/drive-item/driv
10
10
  const drive_folder_service_1 = require("../../services/drive/drive-folder.service");
11
11
  const drive_utils_1 = require("../../utils/drive.utils");
12
12
  const format_utils_1 = require("../../utils/format.utils");
13
- const node_crypto_1 = require("node:crypto");
14
13
  const mime_types_1 = __importDefault(require("mime-types"));
15
14
  const webdav_utils_1 = require("../../utils/webdav.utils");
16
15
  const logger_utils_1 = require("../../utils/logger.utils");
17
16
  const usage_service_1 = require("../../services/usage.service");
17
+ const errors_utils_1 = require("../../utils/errors.utils");
18
18
  class PROPFINDRequestHandler {
19
19
  handle = async (req, res) => {
20
20
  const resource = await webdav_utils_1.WebDavUtils.getRequestedResource(req.url);
21
21
  logger_utils_1.webdavLogger.info(`[PROPFIND] Request received for item at ${resource.url}`);
22
22
  const driveItem = await webdav_utils_1.WebDavUtils.getDriveItemFromResource(resource);
23
23
  if (!driveItem) {
24
- res.status(404).send();
25
- return;
24
+ throw new errors_utils_1.NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
26
25
  }
27
26
  res.set('Content-Type', 'application/xml; charset="utf-8"');
28
27
  switch (driveItem.itemType) {
@@ -143,7 +142,7 @@ class PROPFINDRequestHandler {
143
142
  [xml_utils_1.XMLUtils.addDefaultNamespace('status')]: 'HTTP/1.1 200 OK',
144
143
  [xml_utils_1.XMLUtils.addDefaultNamespace('prop')]: {
145
144
  [xml_utils_1.XMLUtils.addDefaultNamespace('getcontenttype')]: 'httpd/unix-directory',
146
- [xml_utils_1.XMLUtils.addDefaultNamespace('getetag')]: '"' + (0, node_crypto_1.randomUUID)().replaceAll('-', '') + '"',
145
+ [xml_utils_1.XMLUtils.addDefaultNamespace('getetag')]: webdav_utils_1.WebDavUtils.getItemETag(driveFolderItem),
147
146
  'x1:lastmodified': {
148
147
  '#text': format_utils_1.FormatUtils.formatDateForWebDav(driveFolderItem.updatedAt),
149
148
  '@_xmlns:x1': 'SAR:',
@@ -177,7 +176,7 @@ class PROPFINDRequestHandler {
177
176
  [xml_utils_1.XMLUtils.addDefaultNamespace('getlastmodified')]: format_utils_1.FormatUtils.formatDateForWebDav(driveFolderItem.updatedAt),
178
177
  [xml_utils_1.XMLUtils.addDefaultNamespace('getcontentlength')]: 0,
179
178
  [xml_utils_1.XMLUtils.addDefaultNamespace('getcontenttype')]: 'httpd/unix-directory',
180
- [xml_utils_1.XMLUtils.addDefaultNamespace('getetag')]: '"' + (0, node_crypto_1.randomUUID)().replaceAll('-', '') + '"',
179
+ [xml_utils_1.XMLUtils.addDefaultNamespace('getetag')]: webdav_utils_1.WebDavUtils.getItemETag(driveFolderItem),
181
180
  [xml_utils_1.XMLUtils.addDefaultNamespace('resourcetype')]: {
182
181
  [xml_utils_1.XMLUtils.addDefaultNamespace('collection')]: '',
183
182
  },
@@ -195,7 +194,7 @@ class PROPFINDRequestHandler {
195
194
  [xml_utils_1.XMLUtils.addDefaultNamespace('status')]: 'HTTP/1.1 200 OK',
196
195
  [xml_utils_1.XMLUtils.addDefaultNamespace('prop')]: {
197
196
  [xml_utils_1.XMLUtils.addDefaultNamespace('resourcetype')]: '',
198
- [xml_utils_1.XMLUtils.addDefaultNamespace('getetag')]: '"' + (0, node_crypto_1.randomUUID)().replaceAll('-', '') + '"',
197
+ [xml_utils_1.XMLUtils.addDefaultNamespace('getetag')]: webdav_utils_1.WebDavUtils.getItemETag(driveFileItem),
199
198
  [xml_utils_1.XMLUtils.addDefaultNamespace('displayname')]: displayName,
200
199
  [xml_utils_1.XMLUtils.addDefaultNamespace('getcontenttype')]: mime_types_1.default.lookup(displayName) || 'application/octet-stream',
201
200
  [xml_utils_1.XMLUtils.addDefaultNamespace('getlastmodified')]: lastModified,
@@ -1,4 +1,6 @@
1
+ import { Request, Response } from 'express';
1
2
  import { WebDavMethodHandler } from '../../types/webdav.types';
2
3
  export declare class PROPPATCHRequestHandler implements WebDavMethodHandler {
3
- handle(): Promise<void>;
4
+ handle: (req: Request, res: Response) => Promise<void>;
5
+ private readonly getRequestedPropertyNames;
4
6
  }
@@ -1,10 +1,60 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PROPPATCHRequestHandler = void 0;
4
+ const webdav_utils_1 = require("../../utils/webdav.utils");
5
+ const logger_utils_1 = require("../../utils/logger.utils");
6
+ const xml_utils_1 = require("../../utils/xml.utils");
4
7
  const errors_utils_1 = require("../../utils/errors.utils");
5
8
  class PROPPATCHRequestHandler {
6
- async handle() {
7
- throw new errors_utils_1.NotImplementedError('PROPPATCH is not implemented yet.');
8
- }
9
+ handle = async (req, res) => {
10
+ const resource = await webdav_utils_1.WebDavUtils.getRequestedResource(req.url);
11
+ logger_utils_1.webdavLogger.info(`[PROPPATCH] Request received for item at ${resource.url}`);
12
+ const driveItem = await webdav_utils_1.WebDavUtils.getDriveItemFromResource(resource);
13
+ if (!driveItem) {
14
+ throw new errors_utils_1.NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
15
+ }
16
+ const propertyNames = this.getRequestedPropertyNames(req.body);
17
+ const propNode = Object.fromEntries(propertyNames.map((name) => [name, '']));
18
+ const responseXML = xml_utils_1.XMLUtils.toWebDavXML({
19
+ [xml_utils_1.XMLUtils.addDefaultNamespace('response')]: {
20
+ [xml_utils_1.XMLUtils.addDefaultNamespace('href')]: xml_utils_1.XMLUtils.encodeWebDavUri(resource.url),
21
+ [xml_utils_1.XMLUtils.addDefaultNamespace('propstat')]: {
22
+ [xml_utils_1.XMLUtils.addDefaultNamespace('prop')]: propNode,
23
+ [xml_utils_1.XMLUtils.addDefaultNamespace('status')]: 'HTTP/1.1 403 Forbidden',
24
+ },
25
+ },
26
+ }, { suppressEmptyNode: true });
27
+ res.set('Content-Type', 'application/xml; charset="utf-8"');
28
+ res.status(207).send(responseXML);
29
+ };
30
+ getRequestedPropertyNames = (body) => {
31
+ if (typeof body !== 'string' || body.trim().length === 0) {
32
+ return [];
33
+ }
34
+ try {
35
+ const parsed = xml_utils_1.XMLUtils.toJSON(body, { ignoreAttributes: false });
36
+ const propertyUpdate = parsed?.['D:propertyupdate'] ?? parsed?.propertyupdate;
37
+ const sections = [
38
+ propertyUpdate?.['D:set'] ?? propertyUpdate?.set,
39
+ propertyUpdate?.['D:remove'] ?? propertyUpdate?.remove,
40
+ ];
41
+ const names = new Set();
42
+ for (const section of sections) {
43
+ const prop = section?.['D:prop'] ?? section?.prop;
44
+ if (prop && typeof prop === 'object') {
45
+ for (const key of Object.keys(prop)) {
46
+ if (!key.startsWith('@_')) {
47
+ names.add(key);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ return Array.from(names);
53
+ }
54
+ catch (error) {
55
+ logger_utils_1.webdavLogger.warn(`[PROPPATCH] Failed to parse request body: ${error instanceof Error ? error.message : String(error)}`);
56
+ return [];
57
+ }
58
+ };
9
59
  }
10
60
  exports.PROPPATCHRequestHandler = PROPPATCHRequestHandler;
@@ -39,16 +39,10 @@ class PUTRequestHandler {
39
39
  }
40
40
  isReplacement = true;
41
41
  logger_utils_1.webdavLogger.info(`[PUT] File '${resource.name}' already exists in '${resource.path.dir}', it will be replaced...`);
42
- try {
43
- await webdav_utils_1.WebDavUtils.deleteOrTrashItem(driveFileItem);
44
- await drive_item_repository_1.DriveItemRepository.instance.delete([driveFileItem.uuid]);
45
- }
46
- catch {
47
- }
48
42
  }
49
43
  const { user } = await auth_service_1.AuthService.instance.getAuthDetails();
50
44
  const fileType = resource.path.ext.replace('.', '');
51
- const { fileStream, thumbnailStream } = upload_utils_1.UploadUtils.prepareUploadStreams(req, fileType);
45
+ const { fileStream, thumbnailStream } = upload_utils_1.UploadUtils.prepareUploadStreams(req, fileType, contentLength);
52
46
  const { networkFacade, bucket } = await cli_utils_1.CLIUtils.prepareNetwork(user);
53
47
  let fileId;
54
48
  if (contentLength > 0) {
@@ -79,7 +73,7 @@ class PUTRequestHandler {
79
73
  logger_utils_1.webdavLogger.info('[PUT] ✅ File uploaded to network');
80
74
  }
81
75
  const driveTimer = cli_utils_1.CLIUtils.timer();
82
- const file = await drive_file_service_1.DriveFileService.instance.createFile({
76
+ const filePayload = {
83
77
  plainName: resource.path.name,
84
78
  type: fileType,
85
79
  size: contentLength,
@@ -87,7 +81,21 @@ class PUTRequestHandler {
87
81
  fileId,
88
82
  bucket,
89
83
  encryptVersion: types_1.EncryptionVersion.Aes03,
90
- });
84
+ };
85
+ let file;
86
+ if (driveFileItem?.itemType === 'file') {
87
+ try {
88
+ file = await drive_file_service_1.DriveFileService.instance.replaceFile(driveFileItem.uuid, filePayload);
89
+ }
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)}`);
92
+ await webdav_utils_1.WebDavUtils.deleteOrTrashItem(driveFileItem);
93
+ file = await drive_file_service_1.DriveFileService.instance.createFile(filePayload);
94
+ }
95
+ }
96
+ else {
97
+ file = await drive_file_service_1.DriveFileService.instance.createFile(filePayload);
98
+ }
91
99
  timings.driveUpload = driveTimer.stop();
92
100
  await drive_item_repository_1.DriveItemRepository.instance.createOrUpdate([
93
101
  {
@@ -101,7 +109,7 @@ class PUTRequestHandler {
101
109
  const thumbnailTimer = cli_utils_1.CLIUtils.timer();
102
110
  await thumbnail_service_1.ThumbnailService.instance.tryUploadThumbnail({
103
111
  fileUuid: file.uuid,
104
- bufferStream: thumbnailStream,
112
+ input: thumbnailStream?.getBuffer(),
105
113
  fileType,
106
114
  bucket,
107
115
  networkFacade,
@@ -115,6 +123,7 @@ class PUTRequestHandler {
115
123
  const statusCode = isReplacement ? 204 : 201;
116
124
  logger_utils_1.webdavLogger.info(`[PUT] [RESPONSE-${statusCode}] ${resource.url} - Returning ${statusCode} ` +
117
125
  `after ${cli_utils_1.CLIUtils.formatDuration(totalTime)}`);
126
+ res.header('ETag', webdav_utils_1.WebDavUtils.getItemETag(file));
118
127
  res.status(statusCode).send();
119
128
  };
120
129
  }
@@ -18,8 +18,8 @@ const init = async () => {
18
18
  await config_service_1.ConfigService.instance.ensureInternxtLogsDirExists();
19
19
  await database_service_1.DatabaseService.instance.initialize();
20
20
  await database_service_1.DatabaseService.instance.clear();
21
- const { token, workspace } = await auth_service_1.AuthService.instance.getAuthDetails();
22
- sdk_manager_service_1.SdkManager.init({ token, workspaceToken: workspace?.workspaceCredentials.token });
21
+ const { token } = await auth_service_1.AuthService.instance.getAuthDetails();
22
+ sdk_manager_service_1.SdkManager.init({ token });
23
23
  new webdav_server_1.WebDavServer((0, express_1.default)())
24
24
  .start()
25
25
  .then()