d-drive-cli 1.2.0 → 1.3.0

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
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "d-drive-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "D-Drive CLI tool for developers",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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 {