retold-remote 0.0.5 → 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/docs/README.md +181 -0
- package/docs/_cover.md +14 -0
- package/docs/_sidebar.md +10 -0
- package/docs/_topbar.md +3 -0
- package/docs/audio-viewer.md +133 -0
- package/docs/ebook-reader.md +90 -0
- package/docs/image-viewer.md +90 -0
- package/docs/server-setup.md +262 -0
- package/docs/video-viewer.md +134 -0
- package/html/docs.html +59 -0
- package/html/index.html +1 -1
- package/package.json +20 -6
- package/source/Pict-Application-RetoldRemote.js +122 -2
- package/source/RetoldRemote-ExtensionMaps.js +33 -0
- package/source/cli/RetoldRemote-Server-Setup.js +211 -67
- package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
- package/source/providers/Pict-Provider-AISortManager.js +456 -0
- package/source/providers/Pict-Provider-CollectionManager.js +1200 -0
- package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
- package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
- package/source/providers/Pict-Provider-GalleryNavigation.js +187 -409
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
- package/source/providers/Pict-Provider-ToastNotification.js +96 -0
- package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
- package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
- package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
- package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
- package/source/server/RetoldRemote-AISortService.js +879 -0
- package/source/server/RetoldRemote-ArchiveService.js +2 -12
- package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
- package/source/server/RetoldRemote-CollectionService.js +843 -0
- package/source/server/RetoldRemote-EbookService.js +7 -16
- package/source/server/RetoldRemote-FileOperationService.js +560 -0
- package/source/server/RetoldRemote-MediaService.js +15 -14
- package/source/server/RetoldRemote-MetadataCache.js +411 -0
- package/source/server/RetoldRemote-ParimeCache.js +349 -0
- package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
- package/source/server/RetoldRemote-VideoFrameService.js +7 -15
- package/source/views/PictView-Remote-AudioExplorer.js +10 -43
- package/source/views/PictView-Remote-CollectionsPanel.js +1486 -0
- package/source/views/PictView-Remote-Gallery.js +237 -44
- package/source/views/PictView-Remote-ImageViewer.js +1 -34
- package/source/views/PictView-Remote-Layout.js +411 -20
- package/source/views/PictView-Remote-MediaViewer.js +311 -28
- package/source/views/PictView-Remote-SettingsPanel.js +275 -133
- package/source/views/PictView-Remote-TopBar.js +701 -14
- package/source/views/PictView-Remote-VLCSetup.js +8 -39
- package/source/views/PictView-Remote-VideoExplorer.js +20 -54
- package/web-application/css/docuserve.css +73 -0
- package/web-application/css/retold-remote.css +3 -0
- package/web-application/docs/README.md +181 -0
- package/web-application/docs/_cover.md +14 -0
- package/web-application/docs/_sidebar.md +10 -0
- package/web-application/docs/_topbar.md +3 -0
- package/web-application/docs/audio-viewer.md +133 -0
- package/web-application/docs/ebook-reader.md +90 -0
- package/web-application/docs/image-viewer.md +90 -0
- package/web-application/docs/server-setup.md +262 -0
- package/web-application/docs/video-viewer.md +134 -0
- package/web-application/docs.html +59 -0
- package/web-application/index.html +1 -1
- package/web-application/js/pict-docuserve.min.js +58 -0
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +2652 -970
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +60 -13
- package/web-application/retold-remote.min.js.map +1 -1
- package/server.js +0 -43
|
@@ -18,8 +18,7 @@ const libChildProcess = require('child_process');
|
|
|
18
18
|
|
|
19
19
|
const _DefaultServiceConfiguration =
|
|
20
20
|
{
|
|
21
|
-
"ContentPath": "."
|
|
22
|
-
"CachePath": null
|
|
21
|
+
"ContentPath": "."
|
|
23
22
|
};
|
|
24
23
|
|
|
25
24
|
// Extensions that can be converted to EPUB by ebook-convert
|
|
@@ -60,16 +59,7 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
60
59
|
|
|
61
60
|
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
62
61
|
|
|
63
|
-
this.
|
|
64
|
-
|| libPath.join(process.cwd(), 'dist', 'retold-cache', 'ebook-conversions');
|
|
65
|
-
|
|
66
|
-
// Ensure cache directory exists
|
|
67
|
-
if (!libFs.existsSync(this.cachePath))
|
|
68
|
-
{
|
|
69
|
-
libFs.mkdirSync(this.cachePath, { recursive: true });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.fable.log.info(`Ebook Service: cache at ${this.cachePath}`);
|
|
62
|
+
this.fable.log.info('Ebook Service: using ParimeBinaryStorage (category: ebook-cache)');
|
|
73
63
|
}
|
|
74
64
|
|
|
75
65
|
/**
|
|
@@ -96,7 +86,7 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
96
86
|
{
|
|
97
87
|
let tmpInput = `${pAbsPath}:${pMtimeMs}`;
|
|
98
88
|
let tmpHash = libCrypto.createHash('sha256').update(tmpInput).digest('hex').substring(0, 16);
|
|
99
|
-
return
|
|
89
|
+
return this.fable.ParimeBinaryStorage.resolvePath('ebook-cache', tmpHash);
|
|
100
90
|
}
|
|
101
91
|
|
|
102
92
|
/**
|
|
@@ -221,11 +211,12 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
221
211
|
return null;
|
|
222
212
|
}
|
|
223
213
|
|
|
224
|
-
let
|
|
214
|
+
let tmpCacheDir = this.fable.ParimeBinaryStorage.resolvePath('ebook-cache', pCacheKey);
|
|
215
|
+
let tmpPath = libPath.join(tmpCacheDir, pFilename);
|
|
225
216
|
|
|
226
|
-
// Double-check it's under
|
|
217
|
+
// Double-check it's under the storage root
|
|
227
218
|
let tmpResolved = libPath.resolve(tmpPath);
|
|
228
|
-
if (!tmpResolved.startsWith(this.
|
|
219
|
+
if (!tmpResolved.startsWith(this.fable.ParimeBinaryStorage.storageRoot))
|
|
229
220
|
{
|
|
230
221
|
return null;
|
|
231
222
|
}
|
|
@@ -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;
|
|
@@ -18,15 +18,11 @@ const libUrl = require('url');
|
|
|
18
18
|
const libToolDetector = require('./RetoldRemote-ToolDetector.js');
|
|
19
19
|
const libThumbnailCache = require('./RetoldRemote-ThumbnailCache.js');
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
const _VideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true, 'ogv': true, 'mpg': true, 'mpeg': true, 'mpe': true, 'mpv': true, 'm2v': true, 'ts': true, 'mts': true, 'm2ts': true, 'vob': true, '3gp': true, '3g2': true, 'f4v': true, 'rm': true, 'rmvb': true, 'divx': true, 'asf': true, 'mxf': true, 'dv': true, 'nsv': true, 'nuv': true, 'y4m': true, 'wtv': true, 'swf': true, 'dat': true };
|
|
23
|
-
const _AudioExtensions = { 'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true, 'm4a': true, 'wma': true, 'oga': true };
|
|
24
|
-
const _DocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true, 'doc': true, 'docx': true };
|
|
21
|
+
const libExtensionMaps = require('../RetoldRemote-ExtensionMaps.js');
|
|
25
22
|
|
|
26
23
|
const _DefaultServiceConfiguration =
|
|
27
24
|
{
|
|
28
25
|
"ContentPath": ".",
|
|
29
|
-
"ThumbnailCachePath": null,
|
|
30
26
|
"APIRoutePrefix": "/api/media",
|
|
31
27
|
"DefaultThumbnailWidth": 200,
|
|
32
28
|
"DefaultThumbnailHeight": 200
|
|
@@ -51,12 +47,9 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
51
47
|
|
|
52
48
|
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
53
49
|
|
|
54
|
-
let tmpCachePath = this.options.ThumbnailCachePath
|
|
55
|
-
|| libPath.join(process.cwd(), 'dist', 'retold-cache', 'thumbnails');
|
|
56
|
-
|
|
57
50
|
this.toolDetector = new libToolDetector();
|
|
58
51
|
this.capabilities = this.toolDetector.detect();
|
|
59
|
-
this.thumbnailCache = new libThumbnailCache(
|
|
52
|
+
this.thumbnailCache = new libThumbnailCache(this.fable);
|
|
60
53
|
this.pathRegistry = this.options.PathRegistry || null;
|
|
61
54
|
|
|
62
55
|
this.fable.log.info(`Media Service: capabilities = ${JSON.stringify(this.capabilities)}`);
|
|
@@ -95,11 +88,7 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
95
88
|
*/
|
|
96
89
|
_getMediaCategory(pExtension)
|
|
97
90
|
{
|
|
98
|
-
|
|
99
|
-
if (_VideoExtensions[pExtension]) return 'video';
|
|
100
|
-
if (_AudioExtensions[pExtension]) return 'audio';
|
|
101
|
-
if (_DocumentExtensions[pExtension]) return 'document';
|
|
102
|
-
return 'other';
|
|
91
|
+
return libExtensionMaps.getCategory(pExtension);
|
|
103
92
|
}
|
|
104
93
|
|
|
105
94
|
/**
|
|
@@ -265,6 +254,7 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
265
254
|
tmpProbe.Height = pMetadata.height;
|
|
266
255
|
tmpProbe.Codec = pMetadata.codec;
|
|
267
256
|
tmpProbe.Bitrate = pMetadata.bitrate;
|
|
257
|
+
tmpProbe.Tags = pMetadata.tags || {};
|
|
268
258
|
}
|
|
269
259
|
pResponse.send(tmpProbe);
|
|
270
260
|
return fNext();
|
|
@@ -502,6 +492,17 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
502
492
|
{
|
|
503
493
|
tmpResult.duration = parseFloat(tmpData.format.duration) || null;
|
|
504
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
|
+
}
|
|
505
506
|
}
|
|
506
507
|
|
|
507
508
|
// Find video stream for dimensions
|