retold-remote 0.0.6 → 0.0.7
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/css/retold-remote.css +3 -0
- package/html/index.html +1 -1
- package/package.json +1 -1
- package/source/Pict-Application-RetoldRemote.js +21 -2
- package/source/cli/RetoldRemote-Server-Setup.js +129 -0
- package/source/providers/Pict-Provider-AISortManager.js +456 -0
- package/source/providers/Pict-Provider-CollectionManager.js +266 -0
- package/source/server/RetoldRemote-AISortService.js +879 -0
- package/source/server/RetoldRemote-CollectionService.js +161 -2
- package/source/server/RetoldRemote-FileOperationService.js +560 -0
- package/source/server/RetoldRemote-MediaService.js +12 -0
- package/source/server/RetoldRemote-MetadataCache.js +411 -0
- package/source/views/PictView-Remote-CollectionsPanel.js +435 -36
- package/source/views/PictView-Remote-Layout.js +2 -0
- package/source/views/PictView-Remote-SettingsPanel.js +156 -0
- package/source/views/PictView-Remote-TopBar.js +86 -0
- package/web-application/css/retold-remote.css +3 -0
- package/web-application/index.html +1 -1
- package/web-application/retold-remote.js +402 -34
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +32 -15
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- File Operation Service
|
|
3
|
+
*
|
|
4
|
+
* Provides safe file move, directory creation, and undo capabilities.
|
|
5
|
+
* All paths are sanitized and resolved within the content root to
|
|
6
|
+
* prevent directory traversal attacks.
|
|
7
|
+
*
|
|
8
|
+
* Batch moves are recorded in Bibliograph for undo support.
|
|
9
|
+
*
|
|
10
|
+
* API:
|
|
11
|
+
* POST /api/files/mkdir - Create directory recursively
|
|
12
|
+
* POST /api/files/move - Move a single file
|
|
13
|
+
* POST /api/files/move-batch - Move multiple files atomically
|
|
14
|
+
* POST /api/files/undo-batch - Reverse a completed batch
|
|
15
|
+
*
|
|
16
|
+
* @license MIT
|
|
17
|
+
*/
|
|
18
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
19
|
+
const libFs = require('fs');
|
|
20
|
+
const libPath = require('path');
|
|
21
|
+
|
|
22
|
+
const SOURCE_NAME = 'retold-remote-file-ops';
|
|
23
|
+
|
|
24
|
+
const _DefaultServiceConfiguration =
|
|
25
|
+
{
|
|
26
|
+
"ContentPath": ".",
|
|
27
|
+
"MaxBatchSize": 1000
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
class RetoldRemoteFileOperationService extends libFableServiceProviderBase
|
|
31
|
+
{
|
|
32
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
33
|
+
{
|
|
34
|
+
super(pFable, pOptions, pServiceHash);
|
|
35
|
+
|
|
36
|
+
this.serviceType = 'RetoldRemoteFileOperationService';
|
|
37
|
+
|
|
38
|
+
// Merge with defaults
|
|
39
|
+
for (let tmpKey in _DefaultServiceConfiguration)
|
|
40
|
+
{
|
|
41
|
+
if (!(tmpKey in this.options))
|
|
42
|
+
{
|
|
43
|
+
this.options[tmpKey] = _DefaultServiceConfiguration[tmpKey];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
48
|
+
|
|
49
|
+
this.fable.log.info('File Operation Service: initialized');
|
|
50
|
+
this.fable.log.info(` Content root: ${this.contentPath}`);
|
|
51
|
+
this.fable.log.info(` Max batch size: ${this.options.MaxBatchSize}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sanitize a path to prevent directory traversal.
|
|
56
|
+
* Returns null if invalid.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} pPath
|
|
59
|
+
* @returns {string|null}
|
|
60
|
+
*/
|
|
61
|
+
_sanitizePath(pPath)
|
|
62
|
+
{
|
|
63
|
+
if (!pPath || typeof (pPath) !== 'string')
|
|
64
|
+
{
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let tmpPath = decodeURIComponent(pPath).replace(/^\/+/, '');
|
|
69
|
+
|
|
70
|
+
if (tmpPath.includes('..') || libPath.isAbsolute(tmpPath))
|
|
71
|
+
{
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Resolve and verify it stays within content root
|
|
76
|
+
let tmpAbsPath = libPath.join(this.contentPath, tmpPath);
|
|
77
|
+
if (!tmpAbsPath.startsWith(this.contentPath))
|
|
78
|
+
{
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return tmpPath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Move a single file from source to destination within the content root.
|
|
87
|
+
* Creates parent directories as needed.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} pSource - Relative source path
|
|
90
|
+
* @param {string} pDestination - Relative destination path
|
|
91
|
+
* @param {function} fCallback - Callback(pError, { Source, Destination })
|
|
92
|
+
*/
|
|
93
|
+
moveFile(pSource, pDestination, fCallback)
|
|
94
|
+
{
|
|
95
|
+
try
|
|
96
|
+
{
|
|
97
|
+
let tmpSourceRel = this._sanitizePath(pSource);
|
|
98
|
+
let tmpDestRel = this._sanitizePath(pDestination);
|
|
99
|
+
|
|
100
|
+
if (!tmpSourceRel)
|
|
101
|
+
{
|
|
102
|
+
return fCallback(new Error('Invalid source path.'));
|
|
103
|
+
}
|
|
104
|
+
if (!tmpDestRel)
|
|
105
|
+
{
|
|
106
|
+
return fCallback(new Error('Invalid destination path.'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let tmpSourceAbs = libPath.join(this.contentPath, tmpSourceRel);
|
|
110
|
+
let tmpDestAbs = libPath.join(this.contentPath, tmpDestRel);
|
|
111
|
+
|
|
112
|
+
if (!libFs.existsSync(tmpSourceAbs))
|
|
113
|
+
{
|
|
114
|
+
return fCallback(new Error('Source file not found: ' + tmpSourceRel));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Don't overwrite existing files
|
|
118
|
+
if (libFs.existsSync(tmpDestAbs))
|
|
119
|
+
{
|
|
120
|
+
return fCallback(new Error('Destination already exists: ' + tmpDestRel));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create parent directory if needed
|
|
124
|
+
let tmpDestDir = libPath.dirname(tmpDestAbs);
|
|
125
|
+
if (!libFs.existsSync(tmpDestDir))
|
|
126
|
+
{
|
|
127
|
+
libFs.mkdirSync(tmpDestDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Try rename first (fast, same filesystem)
|
|
131
|
+
try
|
|
132
|
+
{
|
|
133
|
+
libFs.renameSync(tmpSourceAbs, tmpDestAbs);
|
|
134
|
+
}
|
|
135
|
+
catch (pRenameError)
|
|
136
|
+
{
|
|
137
|
+
// EXDEV = cross-device link; fall back to copy+unlink
|
|
138
|
+
if (pRenameError.code === 'EXDEV')
|
|
139
|
+
{
|
|
140
|
+
libFs.copyFileSync(tmpSourceAbs, tmpDestAbs);
|
|
141
|
+
libFs.unlinkSync(tmpSourceAbs);
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
{
|
|
145
|
+
throw pRenameError;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return fCallback(null, { Source: tmpSourceRel, Destination: tmpDestRel });
|
|
150
|
+
}
|
|
151
|
+
catch (pError)
|
|
152
|
+
{
|
|
153
|
+
return fCallback(pError);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Move multiple files. Records the batch in Bibliograph for undo.
|
|
159
|
+
*
|
|
160
|
+
* @param {Array<{Source:string, Destination:string}>} pMoves
|
|
161
|
+
* @param {function} fCallback - Callback(pError, { BatchGUID, Completed, Failed })
|
|
162
|
+
*/
|
|
163
|
+
moveBatch(pMoves, fCallback)
|
|
164
|
+
{
|
|
165
|
+
let tmpSelf = this;
|
|
166
|
+
|
|
167
|
+
if (!Array.isArray(pMoves) || pMoves.length === 0)
|
|
168
|
+
{
|
|
169
|
+
return fCallback(new Error('No moves provided.'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (pMoves.length > this.options.MaxBatchSize)
|
|
173
|
+
{
|
|
174
|
+
return fCallback(new Error(`Batch exceeds maximum size of ${this.options.MaxBatchSize}.`));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Pre-validate all paths before moving anything
|
|
178
|
+
let tmpValidatedMoves = [];
|
|
179
|
+
for (let i = 0; i < pMoves.length; i++)
|
|
180
|
+
{
|
|
181
|
+
let tmpMove = pMoves[i];
|
|
182
|
+
let tmpSourceRel = this._sanitizePath(tmpMove.Source);
|
|
183
|
+
let tmpDestRel = this._sanitizePath(tmpMove.Destination);
|
|
184
|
+
|
|
185
|
+
if (!tmpSourceRel || !tmpDestRel)
|
|
186
|
+
{
|
|
187
|
+
return fCallback(new Error(`Invalid path at index ${i}.`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let tmpSourceAbs = libPath.join(this.contentPath, tmpSourceRel);
|
|
191
|
+
if (!libFs.existsSync(tmpSourceAbs))
|
|
192
|
+
{
|
|
193
|
+
return fCallback(new Error(`Source not found at index ${i}: ${tmpSourceRel}`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let tmpDestAbs = libPath.join(this.contentPath, tmpDestRel);
|
|
197
|
+
if (libFs.existsSync(tmpDestAbs))
|
|
198
|
+
{
|
|
199
|
+
return fCallback(new Error(`Destination already exists at index ${i}: ${tmpDestRel}`));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
tmpValidatedMoves.push(
|
|
203
|
+
{
|
|
204
|
+
Source: tmpSourceRel,
|
|
205
|
+
Destination: tmpDestRel,
|
|
206
|
+
SourceAbs: tmpSourceAbs,
|
|
207
|
+
DestAbs: tmpDestAbs
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Execute moves sequentially
|
|
212
|
+
let tmpCompleted = [];
|
|
213
|
+
let tmpFailed = [];
|
|
214
|
+
let tmpIndex = 0;
|
|
215
|
+
|
|
216
|
+
function _moveNext()
|
|
217
|
+
{
|
|
218
|
+
if (tmpIndex >= tmpValidatedMoves.length)
|
|
219
|
+
{
|
|
220
|
+
// Record batch in Bibliograph for undo
|
|
221
|
+
let tmpBatchGUID = tmpSelf.fable.getUUID();
|
|
222
|
+
let tmpBatchRecord =
|
|
223
|
+
{
|
|
224
|
+
GUID: tmpBatchGUID,
|
|
225
|
+
Timestamp: new Date().toISOString(),
|
|
226
|
+
Moves: tmpCompleted,
|
|
227
|
+
FailedCount: tmpFailed.length,
|
|
228
|
+
Status: (tmpFailed.length === 0) ? 'completed' : 'partial'
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
tmpSelf.fable.Bibliograph.write(SOURCE_NAME, tmpBatchGUID, tmpBatchRecord,
|
|
232
|
+
(pWriteError) =>
|
|
233
|
+
{
|
|
234
|
+
if (pWriteError)
|
|
235
|
+
{
|
|
236
|
+
tmpSelf.fable.log.warn(`Failed to record batch: ${pWriteError.message}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return fCallback(null,
|
|
240
|
+
{
|
|
241
|
+
BatchGUID: tmpBatchGUID,
|
|
242
|
+
Completed: tmpCompleted,
|
|
243
|
+
Failed: tmpFailed,
|
|
244
|
+
TotalMoved: tmpCompleted.length,
|
|
245
|
+
TotalFailed: tmpFailed.length
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let tmpMove = tmpValidatedMoves[tmpIndex];
|
|
252
|
+
tmpIndex++;
|
|
253
|
+
|
|
254
|
+
try
|
|
255
|
+
{
|
|
256
|
+
// Create parent directory if needed
|
|
257
|
+
let tmpDestDir = libPath.dirname(tmpMove.DestAbs);
|
|
258
|
+
if (!libFs.existsSync(tmpDestDir))
|
|
259
|
+
{
|
|
260
|
+
libFs.mkdirSync(tmpDestDir, { recursive: true });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Try rename first
|
|
264
|
+
try
|
|
265
|
+
{
|
|
266
|
+
libFs.renameSync(tmpMove.SourceAbs, tmpMove.DestAbs);
|
|
267
|
+
}
|
|
268
|
+
catch (pRenameError)
|
|
269
|
+
{
|
|
270
|
+
if (pRenameError.code === 'EXDEV')
|
|
271
|
+
{
|
|
272
|
+
libFs.copyFileSync(tmpMove.SourceAbs, tmpMove.DestAbs);
|
|
273
|
+
libFs.unlinkSync(tmpMove.SourceAbs);
|
|
274
|
+
}
|
|
275
|
+
else
|
|
276
|
+
{
|
|
277
|
+
throw pRenameError;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
tmpCompleted.push({ Source: tmpMove.Source, Destination: tmpMove.Destination });
|
|
282
|
+
}
|
|
283
|
+
catch (pMoveError)
|
|
284
|
+
{
|
|
285
|
+
tmpFailed.push(
|
|
286
|
+
{
|
|
287
|
+
Source: tmpMove.Source,
|
|
288
|
+
Destination: tmpMove.Destination,
|
|
289
|
+
Error: pMoveError.message
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_moveNext();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Initialize Bibliograph source, then proceed
|
|
297
|
+
this.fable.Bibliograph.createSource(SOURCE_NAME,
|
|
298
|
+
(pCreateError) =>
|
|
299
|
+
{
|
|
300
|
+
// createSource may error if source exists; that's fine
|
|
301
|
+
_moveNext();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Undo a previously executed batch by reversing all moves.
|
|
307
|
+
*
|
|
308
|
+
* @param {string} pBatchGUID
|
|
309
|
+
* @param {function} fCallback - Callback(pError, { Reversed, Failed })
|
|
310
|
+
*/
|
|
311
|
+
undoBatch(pBatchGUID, fCallback)
|
|
312
|
+
{
|
|
313
|
+
let tmpSelf = this;
|
|
314
|
+
|
|
315
|
+
this.fable.Bibliograph.createSource(SOURCE_NAME,
|
|
316
|
+
(pCreateError) =>
|
|
317
|
+
{
|
|
318
|
+
tmpSelf.fable.Bibliograph.read(SOURCE_NAME, pBatchGUID,
|
|
319
|
+
(pReadError, pRecord) =>
|
|
320
|
+
{
|
|
321
|
+
if (pReadError || !pRecord)
|
|
322
|
+
{
|
|
323
|
+
return fCallback(new Error('Batch not found: ' + pBatchGUID));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (pRecord.Status === 'undone')
|
|
327
|
+
{
|
|
328
|
+
return fCallback(new Error('Batch already undone.'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let tmpMoves = pRecord.Moves || [];
|
|
332
|
+
let tmpReversed = [];
|
|
333
|
+
let tmpFailed = [];
|
|
334
|
+
|
|
335
|
+
// Reverse in reverse order
|
|
336
|
+
for (let i = tmpMoves.length - 1; i >= 0; i--)
|
|
337
|
+
{
|
|
338
|
+
let tmpMove = tmpMoves[i];
|
|
339
|
+
|
|
340
|
+
try
|
|
341
|
+
{
|
|
342
|
+
let tmpCurrentAbs = libPath.join(tmpSelf.contentPath, tmpMove.Destination);
|
|
343
|
+
let tmpOriginalAbs = libPath.join(tmpSelf.contentPath, tmpMove.Source);
|
|
344
|
+
|
|
345
|
+
if (!libFs.existsSync(tmpCurrentAbs))
|
|
346
|
+
{
|
|
347
|
+
tmpFailed.push(
|
|
348
|
+
{
|
|
349
|
+
Source: tmpMove.Destination,
|
|
350
|
+
Destination: tmpMove.Source,
|
|
351
|
+
Error: 'File not found at current location'
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Re-create original directory if needed
|
|
357
|
+
let tmpOrigDir = libPath.dirname(tmpOriginalAbs);
|
|
358
|
+
if (!libFs.existsSync(tmpOrigDir))
|
|
359
|
+
{
|
|
360
|
+
libFs.mkdirSync(tmpOrigDir, { recursive: true });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try
|
|
364
|
+
{
|
|
365
|
+
libFs.renameSync(tmpCurrentAbs, tmpOriginalAbs);
|
|
366
|
+
}
|
|
367
|
+
catch (pRenameError)
|
|
368
|
+
{
|
|
369
|
+
if (pRenameError.code === 'EXDEV')
|
|
370
|
+
{
|
|
371
|
+
libFs.copyFileSync(tmpCurrentAbs, tmpOriginalAbs);
|
|
372
|
+
libFs.unlinkSync(tmpCurrentAbs);
|
|
373
|
+
}
|
|
374
|
+
else
|
|
375
|
+
{
|
|
376
|
+
throw pRenameError;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
tmpReversed.push({ Source: tmpMove.Destination, Destination: tmpMove.Source });
|
|
381
|
+
}
|
|
382
|
+
catch (pUndoError)
|
|
383
|
+
{
|
|
384
|
+
tmpFailed.push(
|
|
385
|
+
{
|
|
386
|
+
Source: tmpMove.Destination,
|
|
387
|
+
Destination: tmpMove.Source,
|
|
388
|
+
Error: pUndoError.message
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Update batch record
|
|
394
|
+
pRecord.Status = 'undone';
|
|
395
|
+
pRecord.UndoneAt = new Date().toISOString();
|
|
396
|
+
|
|
397
|
+
tmpSelf.fable.Bibliograph.write(SOURCE_NAME, pBatchGUID, pRecord,
|
|
398
|
+
(pWriteError) =>
|
|
399
|
+
{
|
|
400
|
+
return fCallback(null,
|
|
401
|
+
{
|
|
402
|
+
Reversed: tmpReversed,
|
|
403
|
+
Failed: tmpFailed,
|
|
404
|
+
TotalReversed: tmpReversed.length,
|
|
405
|
+
TotalFailed: tmpFailed.length
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Wire REST endpoints.
|
|
414
|
+
*
|
|
415
|
+
* @param {object} pServiceServer - The Orator service server
|
|
416
|
+
*/
|
|
417
|
+
connectRoutes(pServiceServer)
|
|
418
|
+
{
|
|
419
|
+
let tmpSelf = this;
|
|
420
|
+
let tmpServer = pServiceServer.server;
|
|
421
|
+
|
|
422
|
+
// --- POST /api/files/mkdir ---
|
|
423
|
+
tmpServer.post('/api/files/mkdir',
|
|
424
|
+
(pRequest, pResponse, fNext) =>
|
|
425
|
+
{
|
|
426
|
+
try
|
|
427
|
+
{
|
|
428
|
+
let tmpPath = tmpSelf._sanitizePath(pRequest.body && pRequest.body.Path);
|
|
429
|
+
|
|
430
|
+
if (!tmpPath)
|
|
431
|
+
{
|
|
432
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
433
|
+
return fNext();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let tmpAbsPath = libPath.join(tmpSelf.contentPath, tmpPath);
|
|
437
|
+
|
|
438
|
+
if (libFs.existsSync(tmpAbsPath))
|
|
439
|
+
{
|
|
440
|
+
pResponse.send(200, { Success: true, Path: tmpPath, AlreadyExists: true });
|
|
441
|
+
return fNext();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
libFs.mkdirSync(tmpAbsPath, { recursive: true });
|
|
445
|
+
pResponse.send(200, { Success: true, Path: tmpPath });
|
|
446
|
+
return fNext();
|
|
447
|
+
}
|
|
448
|
+
catch (pError)
|
|
449
|
+
{
|
|
450
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
451
|
+
return fNext();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// --- POST /api/files/move ---
|
|
456
|
+
tmpServer.post('/api/files/move',
|
|
457
|
+
(pRequest, pResponse, fNext) =>
|
|
458
|
+
{
|
|
459
|
+
try
|
|
460
|
+
{
|
|
461
|
+
let tmpSource = pRequest.body && pRequest.body.Source;
|
|
462
|
+
let tmpDest = pRequest.body && pRequest.body.Destination;
|
|
463
|
+
|
|
464
|
+
tmpSelf.moveFile(tmpSource, tmpDest,
|
|
465
|
+
(pError, pResult) =>
|
|
466
|
+
{
|
|
467
|
+
if (pError)
|
|
468
|
+
{
|
|
469
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
470
|
+
return fNext();
|
|
471
|
+
}
|
|
472
|
+
pResponse.send(200, { Success: true, Source: pResult.Source, Destination: pResult.Destination });
|
|
473
|
+
return fNext();
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
catch (pError)
|
|
477
|
+
{
|
|
478
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
479
|
+
return fNext();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// --- POST /api/files/move-batch ---
|
|
484
|
+
tmpServer.post('/api/files/move-batch',
|
|
485
|
+
(pRequest, pResponse, fNext) =>
|
|
486
|
+
{
|
|
487
|
+
try
|
|
488
|
+
{
|
|
489
|
+
let tmpMoves = pRequest.body && pRequest.body.Moves;
|
|
490
|
+
|
|
491
|
+
tmpSelf.moveBatch(tmpMoves,
|
|
492
|
+
(pError, pResult) =>
|
|
493
|
+
{
|
|
494
|
+
if (pError)
|
|
495
|
+
{
|
|
496
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
497
|
+
return fNext();
|
|
498
|
+
}
|
|
499
|
+
pResponse.send(200,
|
|
500
|
+
{
|
|
501
|
+
Success: true,
|
|
502
|
+
BatchGUID: pResult.BatchGUID,
|
|
503
|
+
TotalMoved: pResult.TotalMoved,
|
|
504
|
+
TotalFailed: pResult.TotalFailed,
|
|
505
|
+
Completed: pResult.Completed,
|
|
506
|
+
Failed: pResult.Failed
|
|
507
|
+
});
|
|
508
|
+
return fNext();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (pError)
|
|
512
|
+
{
|
|
513
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
514
|
+
return fNext();
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// --- POST /api/files/undo-batch ---
|
|
519
|
+
tmpServer.post('/api/files/undo-batch',
|
|
520
|
+
(pRequest, pResponse, fNext) =>
|
|
521
|
+
{
|
|
522
|
+
try
|
|
523
|
+
{
|
|
524
|
+
let tmpBatchGUID = pRequest.body && pRequest.body.BatchGUID;
|
|
525
|
+
|
|
526
|
+
if (!tmpBatchGUID)
|
|
527
|
+
{
|
|
528
|
+
pResponse.send(400, { Success: false, Error: 'Missing BatchGUID.' });
|
|
529
|
+
return fNext();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
tmpSelf.undoBatch(tmpBatchGUID,
|
|
533
|
+
(pError, pResult) =>
|
|
534
|
+
{
|
|
535
|
+
if (pError)
|
|
536
|
+
{
|
|
537
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
538
|
+
return fNext();
|
|
539
|
+
}
|
|
540
|
+
pResponse.send(200,
|
|
541
|
+
{
|
|
542
|
+
Success: true,
|
|
543
|
+
TotalReversed: pResult.TotalReversed,
|
|
544
|
+
TotalFailed: pResult.TotalFailed,
|
|
545
|
+
Reversed: pResult.Reversed,
|
|
546
|
+
Failed: pResult.Failed
|
|
547
|
+
});
|
|
548
|
+
return fNext();
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch (pError)
|
|
552
|
+
{
|
|
553
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
554
|
+
return fNext();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
module.exports = RetoldRemoteFileOperationService;
|
|
@@ -254,6 +254,7 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
254
254
|
tmpProbe.Height = pMetadata.height;
|
|
255
255
|
tmpProbe.Codec = pMetadata.codec;
|
|
256
256
|
tmpProbe.Bitrate = pMetadata.bitrate;
|
|
257
|
+
tmpProbe.Tags = pMetadata.tags || {};
|
|
257
258
|
}
|
|
258
259
|
pResponse.send(tmpProbe);
|
|
259
260
|
return fNext();
|
|
@@ -491,6 +492,17 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
491
492
|
{
|
|
492
493
|
tmpResult.duration = parseFloat(tmpData.format.duration) || null;
|
|
493
494
|
tmpResult.bitrate = parseInt(tmpData.format.bit_rate, 10) || null;
|
|
495
|
+
|
|
496
|
+
// Extract format-level tags (ID3, Vorbis comments, etc.)
|
|
497
|
+
if (tmpData.format.tags)
|
|
498
|
+
{
|
|
499
|
+
tmpResult.tags = {};
|
|
500
|
+
let tmpTagKeys = Object.keys(tmpData.format.tags);
|
|
501
|
+
for (let t = 0; t < tmpTagKeys.length; t++)
|
|
502
|
+
{
|
|
503
|
+
tmpResult.tags[tmpTagKeys[t].toLowerCase()] = tmpData.format.tags[tmpTagKeys[t]];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
494
506
|
}
|
|
495
507
|
|
|
496
508
|
// Find video stream for dimensions
|