d-drive-cli 1.2.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,6 +40,8 @@ Upload a single file:
40
40
  d-drive upload ./myfile.txt /backups/
41
41
  ```
42
42
 
43
+ Note: For very large files the server exposes a streaming upload endpoint (`POST /api/files/upload/stream`) that accepts multipart uploads and streams chunks directly to the storage backend without full buffering. Use the API streaming endpoint (see `docs/API.md`) for multi-GB uploads or when you need more robust handling for long uploads.
44
+
43
45
  Upload a directory recursively:
44
46
 
45
47
  ```bash
@@ -0,0 +1 @@
1
+ export declare function copyCommand(remotePath: string): Promise<void>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.copyCommand = copyCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const api_1 = require("../api");
10
+ async function copyCommand(remotePath) {
11
+ const spinner = (0, ora_1.default)('Finding file...').start();
12
+ try {
13
+ const api = (0, api_1.createApiClient)();
14
+ // Find file by path
15
+ const filesResponse = await api.get('/files', { params: { path: remotePath } });
16
+ const files = filesResponse.data;
17
+ if (files.length === 0) {
18
+ spinner.fail(chalk_1.default.red(`File not found: ${remotePath}`));
19
+ process.exit(1);
20
+ }
21
+ const file = files[0];
22
+ spinner.text = 'Creating copy...';
23
+ const resp = await api.post(`/files/${file.id}/copy`);
24
+ spinner.succeed(chalk_1.default.green('Copy created'));
25
+ const created = resp.data;
26
+ console.log(chalk_1.default.gray(`Created: ${created.path || `/${created.name}`}`));
27
+ }
28
+ catch (error) {
29
+ spinner.fail(chalk_1.default.red('Copy failed'));
30
+ console.error(chalk_1.default.red(error.response?.data?.error || error.message));
31
+ process.exit(1);
32
+ }
33
+ }
@@ -13,6 +13,7 @@ const form_data_1 = __importDefault(require("form-data"));
13
13
  const progress_1 = __importDefault(require("progress"));
14
14
  const api_1 = require("../api");
15
15
  const glob_1 = require("glob");
16
+ const axios_1 = __importDefault(require("axios"));
16
17
  async function uploadCommand(source, destination = '/', options) {
17
18
  const spinner = (0, ora_1.default)('Preparing upload...').start();
18
19
  try {
@@ -71,48 +72,105 @@ async function uploadSingleFile(api, filePath, destination, showProgress) {
71
72
  filename: fileName,
72
73
  knownLength: fileSize,
73
74
  });
74
- formData.append('path', destination);
75
+ // Resolve destination directory to a parentId and send parentId (server-authoritative)
76
+ const parentDir = path_1.default.posix.dirname(destination || '/');
77
+ let parentId = null;
78
+ if (parentDir && parentDir !== '/' && parentDir !== '.') {
79
+ parentId = await ensureFolderExists(api, parentDir);
80
+ }
81
+ if (parentId) {
82
+ formData.append('parentId', parentId);
83
+ }
75
84
  // Ensure CLI uploads follow frontend behavior and request server-side encryption by default
76
85
  formData.append('encrypt', 'true');
77
86
  let progressBar = null;
78
87
  if (showProgress) {
88
+ // Use a byte-counting progress bar so we can display an explicit percent.
79
89
  progressBar = new progress_1.default('[:bar] :percent :etas', {
80
90
  complete: '█',
81
91
  incomplete: '░',
82
92
  width: 40,
83
- total: 1,
93
+ total: fileSize,
84
94
  });
85
- // Track bytes read from disk and update progress bar as fraction [0..1]
86
- let uploaded = 0;
95
+ // Track bytes read from disk and update progress bar by bytes.
87
96
  fileStream.on('data', (chunk) => {
88
97
  const len = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
89
- uploaded += len;
90
- if (progressBar) {
91
- const ratio = Math.min(1, uploaded / fileSize);
92
- progressBar.update(ratio);
98
+ if (progressBar)
99
+ progressBar.tick(len);
100
+ });
101
+ // When local read finishes, indicate we're waiting for the server to finish
102
+ fileStream.on('end', () => {
103
+ if (progressBar && !progressBar.complete) {
104
+ // ensure the bar shows very near completion but leave finalizing to server response
105
+ try {
106
+ progressBar.update(Math.min(1, (progressBar.curr || 0) / (progressBar.total || 1)));
107
+ }
108
+ catch (_) { }
93
109
  }
110
+ console.log(chalk_1.default.gray('\nLocal file read complete — waiting for server to finish...'));
94
111
  });
95
112
  }
96
- // Ensure Content-Length is set for axios in Node
113
+ // Use streaming upload endpoint. Do not force Content-Length so the request
114
+ // can stream large files without buffering the whole body in memory.
97
115
  const headers = formData.getHeaders();
98
- try {
99
- const length = await new Promise((resolve, reject) => {
100
- formData.getLength((err, len) => {
101
- if (err)
102
- return reject(err);
103
- resolve(len);
104
- });
105
- });
106
- headers['Content-Length'] = String(length);
107
- }
108
- catch (err) {
109
- // ignore length error
110
- }
111
- await api.post('/files/upload', formData, {
116
+ // axios in Node needs the adapter to handle stream form-data; use api (axios instance)
117
+ await api.post('/files/upload/stream', formData, {
112
118
  headers,
113
119
  maxContentLength: Infinity,
114
120
  maxBodyLength: Infinity,
121
+ // Do not set a timeout for potentially long uploads
122
+ timeout: 0,
123
+ // Allow axios to stream the form-data
124
+ transitional: { forcedJSONParsing: false },
115
125
  });
126
+ // Upload complete (server has processed). If progress bar exists, ensure it shows 100%.
127
+ if (progressBar && !progressBar.complete) {
128
+ try {
129
+ progressBar.update(progressBar.total || 1);
130
+ }
131
+ catch (_) { }
132
+ }
133
+ console.log(chalk_1.default.green('\nUpload finished'));
134
+ }
135
+ // Ensure the directory at `dirPath` exists. Returns the `id` of the directory or null for root.
136
+ async function ensureFolderExists(api, dirPath) {
137
+ // Normalize and split
138
+ const normalized = path_1.default.posix.normalize(dirPath);
139
+ if (normalized === '/' || normalized === '.' || normalized === '')
140
+ return null;
141
+ const segments = normalized.split('/').filter(Boolean);
142
+ let currentPath = '';
143
+ let parentId = null;
144
+ for (const seg of segments) {
145
+ currentPath = `${currentPath}/${seg}`;
146
+ try {
147
+ const resp = await api.get('/files', { params: { path: currentPath } });
148
+ const items = resp.data;
149
+ const dir = items.find(i => i.type === 'DIRECTORY');
150
+ if (dir) {
151
+ parentId = dir.id;
152
+ continue;
153
+ }
154
+ // Not found — create it
155
+ const createResp = await api.post('/files/directory', { name: seg, parentId: parentId || null, path: currentPath });
156
+ parentId = createResp.data.id;
157
+ }
158
+ catch (err) {
159
+ // If a 409 or other error occurs, try to re-query; otherwise rethrow
160
+ if (axios_1.default.isAxiosError(err) && err.response) {
161
+ // Retry by querying again in case of race
162
+ const retry = await api.get('/files', { params: { path: currentPath } });
163
+ const items = retry.data;
164
+ const dir = items.find(i => i.type === 'DIRECTORY');
165
+ if (dir) {
166
+ parentId = dir.id;
167
+ continue;
168
+ }
169
+ }
170
+ throw err;
171
+ }
172
+ }
173
+ return parentId;
116
174
  }
117
175
  function formatFileSize(bytes) {
118
176
  if (bytes === 0)
package/dist/index.js CHANGED
@@ -11,11 +11,13 @@ const upload_1 = require("./commands/upload");
11
11
  const download_1 = require("./commands/download");
12
12
  const list_1 = require("./commands/list");
13
13
  const delete_1 = require("./commands/delete");
14
+ const copy_1 = require("./commands/copy");
15
+ const pkg = require('../package.json');
14
16
  const program = new commander_1.Command();
15
17
  program
16
18
  .name('d-drive')
17
19
  .description('D-Drive CLI - Discord-based cloud storage for developers')
18
- .version('1.0.0');
20
+ .version(pkg.version || '0.0.0');
19
21
  // Config command
20
22
  program
21
23
  .command('config')
@@ -52,6 +54,11 @@ program
52
54
  .option('-r, --recursive', 'Delete directory recursively')
53
55
  .option('-f, --force', 'Force deletion without confirmation')
54
56
  .action(delete_1.deleteCommand);
57
+ // Copy command
58
+ program
59
+ .command('copy <path>')
60
+ .description('Make a copy of a file in-place (auto-numbered)')
61
+ .action(copy_1.copyCommand);
55
62
  // Help command
56
63
  program.on('--help', () => {
57
64
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "d-drive-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.2",
4
4
  "description": "D-Drive CLI tool for developers",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createApiClient } from '../api';
4
+
5
+ export async function copyCommand(remotePath: string) {
6
+ const spinner = ora('Finding file...').start();
7
+
8
+ try {
9
+ const api = createApiClient();
10
+
11
+ // Find file by path
12
+ const filesResponse = await api.get('/files', { params: { path: remotePath } });
13
+ const files = filesResponse.data;
14
+ if (files.length === 0) {
15
+ spinner.fail(chalk.red(`File not found: ${remotePath}`));
16
+ process.exit(1);
17
+ }
18
+
19
+ const file = files[0];
20
+ spinner.text = 'Creating copy...';
21
+
22
+ const resp = await api.post(`/files/${file.id}/copy`);
23
+
24
+ spinner.succeed(chalk.green('Copy created'));
25
+ const created = resp.data;
26
+ console.log(chalk.gray(`Created: ${created.path || `/${created.name}`}`));
27
+ } catch (error: any) {
28
+ spinner.fail(chalk.red('Copy failed'));
29
+ console.error(chalk.red(error.response?.data?.error || error.message));
30
+ process.exit(1);
31
+ }
32
+ }
@@ -7,6 +7,7 @@ import FormData from 'form-data';
7
7
  import ProgressBar from 'progress';
8
8
  import { createApiClient } from '../api';
9
9
  import { glob } from 'glob';
10
+ import axios from 'axios';
10
11
 
11
12
  interface UploadOptions {
12
13
  recursive?: boolean;
@@ -95,51 +96,104 @@ async function uploadSingleFile(
95
96
  filename: fileName,
96
97
  knownLength: fileSize,
97
98
  });
98
- formData.append('path', destination);
99
+ // Resolve destination directory to a parentId and send parentId (server-authoritative)
100
+ const parentDir = path.posix.dirname(destination || '/');
101
+ let parentId: string | null = null;
102
+ if (parentDir && parentDir !== '/' && parentDir !== '.') {
103
+ parentId = await ensureFolderExists(api, parentDir);
104
+ }
105
+ if (parentId) {
106
+ formData.append('parentId', parentId);
107
+ }
99
108
  // Ensure CLI uploads follow frontend behavior and request server-side encryption by default
100
109
  formData.append('encrypt', 'true');
101
110
 
102
111
  let progressBar: ProgressBar | null = null;
103
112
 
104
113
  if (showProgress) {
114
+ // Use a byte-counting progress bar so we can display an explicit percent.
105
115
  progressBar = new ProgressBar('[:bar] :percent :etas', {
106
116
  complete: '█',
107
117
  incomplete: '░',
108
118
  width: 40,
109
- total: 1,
119
+ total: fileSize,
110
120
  });
111
121
 
112
- // Track bytes read from disk and update progress bar as fraction [0..1]
113
- let uploaded = 0;
122
+ // Track bytes read from disk and update progress bar by bytes.
114
123
  fileStream.on('data', (chunk: Buffer | string) => {
115
124
  const len = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
116
- uploaded += len;
117
- if (progressBar) {
118
- const ratio = Math.min(1, uploaded / fileSize);
119
- progressBar.update(ratio);
125
+ if (progressBar) progressBar.tick(len);
126
+ });
127
+ // When local read finishes, indicate we're waiting for the server to finish
128
+ fileStream.on('end', () => {
129
+ if (progressBar && !progressBar.complete) {
130
+ // ensure the bar shows very near completion but leave finalizing to server response
131
+ try { progressBar.update(Math.min(1, (progressBar.curr || 0) / (progressBar.total || 1))); } catch (_) {}
120
132
  }
133
+ console.log(chalk.gray('\nLocal file read complete — waiting for server to finish...'));
121
134
  });
122
135
  }
123
136
 
124
- // Ensure Content-Length is set for axios in Node
137
+ // Use streaming upload endpoint. Do not force Content-Length so the request
138
+ // can stream large files without buffering the whole body in memory.
125
139
  const headers = formData.getHeaders();
126
- try {
127
- const length = await new Promise<number>((resolve, reject) => {
128
- formData.getLength((err: any, len: number) => {
129
- if (err) return reject(err);
130
- resolve(len);
131
- });
132
- });
133
- headers['Content-Length'] = String(length);
134
- } catch (err) {
135
- // ignore length error
136
- }
137
-
138
- await api.post('/files/upload', formData, {
140
+ // axios in Node needs the adapter to handle stream form-data; use api (axios instance)
141
+ await api.post('/files/upload/stream', formData, {
139
142
  headers,
140
143
  maxContentLength: Infinity,
141
144
  maxBodyLength: Infinity,
145
+ // Do not set a timeout for potentially long uploads
146
+ timeout: 0,
147
+ // Allow axios to stream the form-data
148
+ transitional: { forcedJSONParsing: false },
142
149
  });
150
+ // Upload complete (server has processed). If progress bar exists, ensure it shows 100%.
151
+ if (progressBar && !progressBar.complete) {
152
+ try { progressBar.update(progressBar.total || 1); } catch (_) {}
153
+ }
154
+ console.log(chalk.green('\nUpload finished'));
155
+ }
156
+
157
+ // Ensure the directory at `dirPath` exists. Returns the `id` of the directory or null for root.
158
+ async function ensureFolderExists(api: any, dirPath: string): Promise<string | null> {
159
+ // Normalize and split
160
+ const normalized = path.posix.normalize(dirPath);
161
+ if (normalized === '/' || normalized === '.' || normalized === '') return null;
162
+
163
+ const segments = normalized.split('/').filter(Boolean);
164
+ let currentPath = '';
165
+ let parentId: string | null = null;
166
+
167
+ for (const seg of segments) {
168
+ currentPath = `${currentPath}/${seg}`;
169
+ try {
170
+ const resp = await api.get('/files', { params: { path: currentPath } });
171
+ const items = resp.data as any[];
172
+ const dir = items.find(i => i.type === 'DIRECTORY');
173
+ if (dir) {
174
+ parentId = dir.id;
175
+ continue;
176
+ }
177
+ // Not found — create it
178
+ const createResp = await api.post('/files/directory', { name: seg, parentId: parentId || null, path: currentPath });
179
+ parentId = createResp.data.id;
180
+ } catch (err: any) {
181
+ // If a 409 or other error occurs, try to re-query; otherwise rethrow
182
+ if (axios.isAxiosError(err) && err.response) {
183
+ // Retry by querying again in case of race
184
+ const retry = await api.get('/files', { params: { path: currentPath } });
185
+ const items = retry.data as any[];
186
+ const dir = items.find(i => i.type === 'DIRECTORY');
187
+ if (dir) {
188
+ parentId = dir.id;
189
+ continue;
190
+ }
191
+ }
192
+ throw err;
193
+ }
194
+ }
195
+
196
+ return parentId;
143
197
  }
144
198
 
145
199
  function formatFileSize(bytes: number): string {
package/src/index.ts CHANGED
@@ -7,13 +7,15 @@ import { uploadCommand } from './commands/upload';
7
7
  import { downloadCommand } from './commands/download';
8
8
  import { listCommand } from './commands/list';
9
9
  import { deleteCommand } from './commands/delete';
10
+ import { copyCommand } from './commands/copy';
11
+ const pkg = require('../package.json');
10
12
 
11
13
  const program = new Command();
12
14
 
13
15
  program
14
16
  .name('d-drive')
15
17
  .description('D-Drive CLI - Discord-based cloud storage for developers')
16
- .version('1.0.0');
18
+ .version(pkg.version || '0.0.0');
17
19
 
18
20
  // Config command
19
21
  program
@@ -56,6 +58,12 @@ program
56
58
  .option('-f, --force', 'Force deletion without confirmation')
57
59
  .action(deleteCommand);
58
60
 
61
+ // Copy command
62
+ program
63
+ .command('copy <path>')
64
+ .description('Make a copy of a file in-place (auto-numbered)')
65
+ .action(copyCommand);
66
+
59
67
  // Help command
60
68
  program.on('--help', () => {
61
69
  console.log('');