retold-remote 0.0.4 → 0.0.6
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/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/package.json +21 -7
- package/source/Pict-Application-RetoldRemote.js +143 -2
- package/source/RetoldRemote-ExtensionMaps.js +33 -0
- package/source/cli/RetoldRemote-Server-Setup.js +82 -67
- package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
- package/source/providers/Pict-Provider-CollectionManager.js +934 -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 +270 -353
- 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-ArchiveService.js +2 -12
- package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
- package/source/server/RetoldRemote-CollectionService.js +684 -0
- package/source/server/RetoldRemote-EbookService.js +7 -16
- package/source/server/RetoldRemote-MediaService.js +3 -14
- 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 +1087 -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 +410 -20
- package/source/views/PictView-Remote-MediaViewer.js +338 -51
- package/source/views/PictView-Remote-SettingsPanel.js +155 -138
- package/source/views/PictView-Remote-TopBar.js +615 -14
- package/source/views/PictView-Remote-VLCSetup.js +766 -0
- package/source/views/PictView-Remote-VideoExplorer.js +20 -54
- package/web-application/css/docuserve.css +73 -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/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 +2558 -439
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +41 -11
- 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
|
}
|
|
@@ -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 };
|
|
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
|
/**
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- Parime Cache Adapter
|
|
3
|
+
*
|
|
4
|
+
* Wraps parime's ParimeBinaryStorage as a unified cache layer for
|
|
5
|
+
* retold-remote. Supports two modes:
|
|
6
|
+
*
|
|
7
|
+
* 1. Embedded (default): ParimeBinaryStorage accessed directly in-process.
|
|
8
|
+
* 2. Remote: If fable.settings.ParimeCacheServer is a URL, all cache
|
|
9
|
+
* operations route through HTTP to a remote parime server.
|
|
10
|
+
*
|
|
11
|
+
* This adapter provides helper methods for cache key generation and
|
|
12
|
+
* passthrough access to the underlying storage.
|
|
13
|
+
*/
|
|
14
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
15
|
+
const libCrypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
class RetoldRemoteParimeCache extends libFableServiceProviderBase
|
|
18
|
+
{
|
|
19
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
20
|
+
{
|
|
21
|
+
super(pFable, pOptions, pServiceHash);
|
|
22
|
+
|
|
23
|
+
this.serviceType = 'RetoldRemoteParimeCache';
|
|
24
|
+
|
|
25
|
+
this._remoteURL = (typeof(this.fable.settings.ParimeCacheServer) === 'string'
|
|
26
|
+
&& this.fable.settings.ParimeCacheServer.length > 0)
|
|
27
|
+
? this.fable.settings.ParimeCacheServer.replace(/\/+$/, '')
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether this adapter is operating in remote mode.
|
|
33
|
+
*
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
get isRemote()
|
|
37
|
+
{
|
|
38
|
+
return this._remoteURL !== null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build a cache key from an array of components.
|
|
43
|
+
*
|
|
44
|
+
* Components are joined with ':' and hashed with SHA-256.
|
|
45
|
+
* Optionally truncate to pHashLength characters.
|
|
46
|
+
*
|
|
47
|
+
* @param {Array<string|number>} pComponents - Values to include in the key.
|
|
48
|
+
* @param {number} [pHashLength] - Truncate the hash to this many hex chars.
|
|
49
|
+
* @returns {string} The hex hash.
|
|
50
|
+
*/
|
|
51
|
+
buildCacheKey(pComponents, pHashLength)
|
|
52
|
+
{
|
|
53
|
+
let tmpInput = pComponents.join(':');
|
|
54
|
+
let tmpHash = libCrypto.createHash('sha256').update(tmpInput).digest('hex');
|
|
55
|
+
if (typeof(pHashLength) === 'number' && pHashLength > 0)
|
|
56
|
+
{
|
|
57
|
+
return tmpHash.substring(0, pHashLength);
|
|
58
|
+
}
|
|
59
|
+
return tmpHash;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the underlying ParimeBinaryStorage service.
|
|
64
|
+
*
|
|
65
|
+
* @returns {object} The ParimeBinaryStorage instance.
|
|
66
|
+
*/
|
|
67
|
+
get storage()
|
|
68
|
+
{
|
|
69
|
+
return this.fable.ParimeBinaryStorage;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a category + key to an absolute filesystem path.
|
|
74
|
+
*
|
|
75
|
+
* Only valid in embedded mode (not remote).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} pCategory - The cache category.
|
|
78
|
+
* @param {string} pKey - The cache key.
|
|
79
|
+
* @returns {string} Absolute file path.
|
|
80
|
+
*/
|
|
81
|
+
resolvePath(pCategory, pKey)
|
|
82
|
+
{
|
|
83
|
+
return this.fable.ParimeBinaryStorage.resolvePath(pCategory, pKey);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a cached item exists.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} pCategory - The cache category.
|
|
90
|
+
* @param {string} pKey - The cache key.
|
|
91
|
+
* @param {function} fCallback - Callback(pError, pExists).
|
|
92
|
+
*/
|
|
93
|
+
exists(pCategory, pKey, fCallback)
|
|
94
|
+
{
|
|
95
|
+
if (this._remoteURL)
|
|
96
|
+
{
|
|
97
|
+
return this._remoteExists(pCategory, pKey, fCallback);
|
|
98
|
+
}
|
|
99
|
+
return this.fable.ParimeBinaryStorage.exists(pCategory, pKey, fCallback);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read a cached item.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} pCategory - The cache category.
|
|
106
|
+
* @param {string} pKey - The cache key.
|
|
107
|
+
* @param {function} fCallback - Callback(pError, pBuffer).
|
|
108
|
+
*/
|
|
109
|
+
read(pCategory, pKey, fCallback)
|
|
110
|
+
{
|
|
111
|
+
if (this._remoteURL)
|
|
112
|
+
{
|
|
113
|
+
return this._remoteRead(pCategory, pKey, fCallback);
|
|
114
|
+
}
|
|
115
|
+
return this.fable.ParimeBinaryStorage.read(pCategory, pKey, fCallback);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Write a cached item.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} pCategory - The cache category.
|
|
122
|
+
* @param {string} pKey - The cache key.
|
|
123
|
+
* @param {Buffer} pBuffer - The data to write.
|
|
124
|
+
* @param {function} fCallback - Callback(pError).
|
|
125
|
+
*/
|
|
126
|
+
write(pCategory, pKey, pBuffer, fCallback)
|
|
127
|
+
{
|
|
128
|
+
if (this._remoteURL)
|
|
129
|
+
{
|
|
130
|
+
return this._remoteWrite(pCategory, pKey, pBuffer, fCallback);
|
|
131
|
+
}
|
|
132
|
+
return this.fable.ParimeBinaryStorage.write(pCategory, pKey, pBuffer, fCallback);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get a readable stream for a cached item.
|
|
137
|
+
*
|
|
138
|
+
* Only valid in embedded mode.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} pCategory - The cache category.
|
|
141
|
+
* @param {string} pKey - The cache key.
|
|
142
|
+
* @param {object} [pOptions] - Stream options ({ start, end }).
|
|
143
|
+
* @returns {ReadStream}
|
|
144
|
+
*/
|
|
145
|
+
readStream(pCategory, pKey, pOptions)
|
|
146
|
+
{
|
|
147
|
+
return this.fable.ParimeBinaryStorage.readStream(pCategory, pKey, pOptions);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get file stats for a cached item.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} pCategory - The cache category.
|
|
154
|
+
* @param {string} pKey - The cache key.
|
|
155
|
+
* @param {function} fCallback - Callback(pError, pStats).
|
|
156
|
+
*/
|
|
157
|
+
stat(pCategory, pKey, fCallback)
|
|
158
|
+
{
|
|
159
|
+
if (this._remoteURL)
|
|
160
|
+
{
|
|
161
|
+
return this._remoteStat(pCategory, pKey, fCallback);
|
|
162
|
+
}
|
|
163
|
+
return this.fable.ParimeBinaryStorage.stat(pCategory, pKey, fCallback);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Delete a cached item.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} pCategory - The cache category.
|
|
170
|
+
* @param {string} pKey - The cache key.
|
|
171
|
+
* @param {function} fCallback - Callback(pError).
|
|
172
|
+
*/
|
|
173
|
+
delete(pCategory, pKey, fCallback)
|
|
174
|
+
{
|
|
175
|
+
if (this._remoteURL)
|
|
176
|
+
{
|
|
177
|
+
return this._remoteDelete(pCategory, pKey, fCallback);
|
|
178
|
+
}
|
|
179
|
+
return this.fable.ParimeBinaryStorage.delete(pCategory, pKey, fCallback);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Remote mode helpers ──────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
_remoteExists(pCategory, pKey, fCallback)
|
|
188
|
+
{
|
|
189
|
+
let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}/Stat`;
|
|
190
|
+
let libHTTP = require('http');
|
|
191
|
+
let tmpParsed = new URL(tmpURL);
|
|
192
|
+
|
|
193
|
+
let tmpReq = libHTTP.request(
|
|
194
|
+
{
|
|
195
|
+
hostname: tmpParsed.hostname,
|
|
196
|
+
port: tmpParsed.port,
|
|
197
|
+
path: tmpParsed.pathname,
|
|
198
|
+
method: 'GET'
|
|
199
|
+
},
|
|
200
|
+
(pResponse) =>
|
|
201
|
+
{
|
|
202
|
+
let tmpChunks = [];
|
|
203
|
+
pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
|
|
204
|
+
pResponse.on('end', () =>
|
|
205
|
+
{
|
|
206
|
+
return fCallback(null, pResponse.statusCode === 200);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
tmpReq.on('error', (pError) => { return fCallback(pError); });
|
|
210
|
+
tmpReq.end();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
_remoteRead(pCategory, pKey, fCallback)
|
|
217
|
+
{
|
|
218
|
+
let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}`;
|
|
219
|
+
let libHTTP = require('http');
|
|
220
|
+
let tmpParsed = new URL(tmpURL);
|
|
221
|
+
|
|
222
|
+
let tmpReq = libHTTP.request(
|
|
223
|
+
{
|
|
224
|
+
hostname: tmpParsed.hostname,
|
|
225
|
+
port: tmpParsed.port,
|
|
226
|
+
path: tmpParsed.pathname,
|
|
227
|
+
method: 'GET'
|
|
228
|
+
},
|
|
229
|
+
(pResponse) =>
|
|
230
|
+
{
|
|
231
|
+
if (pResponse.statusCode === 404)
|
|
232
|
+
{
|
|
233
|
+
pResponse.resume();
|
|
234
|
+
return fCallback(null, null);
|
|
235
|
+
}
|
|
236
|
+
let tmpChunks = [];
|
|
237
|
+
pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
|
|
238
|
+
pResponse.on('end', () =>
|
|
239
|
+
{
|
|
240
|
+
return fCallback(null, Buffer.concat(tmpChunks));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
tmpReq.on('error', (pError) => { return fCallback(pError); });
|
|
244
|
+
tmpReq.end();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
_remoteWrite(pCategory, pKey, pBuffer, fCallback)
|
|
251
|
+
{
|
|
252
|
+
let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}`;
|
|
253
|
+
let libHTTP = require('http');
|
|
254
|
+
let tmpParsed = new URL(tmpURL);
|
|
255
|
+
|
|
256
|
+
let tmpReq = libHTTP.request(
|
|
257
|
+
{
|
|
258
|
+
hostname: tmpParsed.hostname,
|
|
259
|
+
port: tmpParsed.port,
|
|
260
|
+
path: tmpParsed.pathname,
|
|
261
|
+
method: 'PUT',
|
|
262
|
+
headers: { 'Content-Type': 'application/octet-stream' }
|
|
263
|
+
},
|
|
264
|
+
(pResponse) =>
|
|
265
|
+
{
|
|
266
|
+
pResponse.resume();
|
|
267
|
+
pResponse.on('end', () =>
|
|
268
|
+
{
|
|
269
|
+
return fCallback(pResponse.statusCode >= 400
|
|
270
|
+
? new Error(`Remote write failed: ${pResponse.statusCode}`)
|
|
271
|
+
: null);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
tmpReq.on('error', (pError) => { return fCallback(pError); });
|
|
275
|
+
tmpReq.write(pBuffer);
|
|
276
|
+
tmpReq.end();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @private
|
|
281
|
+
*/
|
|
282
|
+
_remoteStat(pCategory, pKey, fCallback)
|
|
283
|
+
{
|
|
284
|
+
let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}/Stat`;
|
|
285
|
+
let libHTTP = require('http');
|
|
286
|
+
let tmpParsed = new URL(tmpURL);
|
|
287
|
+
|
|
288
|
+
let tmpReq = libHTTP.request(
|
|
289
|
+
{
|
|
290
|
+
hostname: tmpParsed.hostname,
|
|
291
|
+
port: tmpParsed.port,
|
|
292
|
+
path: tmpParsed.pathname,
|
|
293
|
+
method: 'GET'
|
|
294
|
+
},
|
|
295
|
+
(pResponse) =>
|
|
296
|
+
{
|
|
297
|
+
let tmpChunks = [];
|
|
298
|
+
pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
|
|
299
|
+
pResponse.on('end', () =>
|
|
300
|
+
{
|
|
301
|
+
if (pResponse.statusCode === 404)
|
|
302
|
+
{
|
|
303
|
+
return fCallback(null, null);
|
|
304
|
+
}
|
|
305
|
+
try
|
|
306
|
+
{
|
|
307
|
+
let tmpBody = JSON.parse(Buffer.concat(tmpChunks).toString());
|
|
308
|
+
return fCallback(null, { size: tmpBody.Size });
|
|
309
|
+
}
|
|
310
|
+
catch (pError)
|
|
311
|
+
{
|
|
312
|
+
return fCallback(pError);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
tmpReq.on('error', (pError) => { return fCallback(pError); });
|
|
317
|
+
tmpReq.end();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @private
|
|
322
|
+
*/
|
|
323
|
+
_remoteDelete(pCategory, pKey, fCallback)
|
|
324
|
+
{
|
|
325
|
+
let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}`;
|
|
326
|
+
let libHTTP = require('http');
|
|
327
|
+
let tmpParsed = new URL(tmpURL);
|
|
328
|
+
|
|
329
|
+
let tmpReq = libHTTP.request(
|
|
330
|
+
{
|
|
331
|
+
hostname: tmpParsed.hostname,
|
|
332
|
+
port: tmpParsed.port,
|
|
333
|
+
path: tmpParsed.pathname,
|
|
334
|
+
method: 'DELETE'
|
|
335
|
+
},
|
|
336
|
+
(pResponse) =>
|
|
337
|
+
{
|
|
338
|
+
pResponse.resume();
|
|
339
|
+
pResponse.on('end', () =>
|
|
340
|
+
{
|
|
341
|
+
return fCallback(null);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
tmpReq.on('error', (pError) => { return fCallback(pError); });
|
|
345
|
+
tmpReq.end();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = RetoldRemoteParimeCache;
|
|
@@ -1,45 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Retold Remote -- Filesystem Thumbnail Cache
|
|
3
3
|
*
|
|
4
|
-
* Caches generated thumbnails
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Caches generated thumbnails using Parime's ParimeBinaryStorage.
|
|
5
|
+
* Cache keys use a two-level structure: a folder hash derived from
|
|
6
|
+
* the source file's directory, and a file-specific hash derived from
|
|
7
|
+
* the filename, modification time, and requested dimensions.
|
|
8
|
+
*
|
|
9
|
+
* This means all thumbnails for images in the same folder are
|
|
10
|
+
* co-located under the same shard directory — browsing a gallery
|
|
11
|
+
* folder of 10,000 images creates entries in ONE shard directory
|
|
12
|
+
* rather than 10,000 separate shard paths.
|
|
13
|
+
*
|
|
14
|
+
* Stale entries are automatically invalidated when the source file
|
|
15
|
+
* changes (because mtime is part of the file-specific hash).
|
|
8
16
|
*/
|
|
9
17
|
const libFs = require('fs');
|
|
10
18
|
const libPath = require('path');
|
|
11
19
|
const libCrypto = require('crypto');
|
|
12
20
|
|
|
21
|
+
const _CATEGORY = 'thumbnails';
|
|
22
|
+
|
|
13
23
|
class ThumbnailCache
|
|
14
24
|
{
|
|
15
25
|
/**
|
|
16
|
-
* @param {
|
|
26
|
+
* @param {object} pFable - The Fable instance (must have ParimeBinaryStorage wired)
|
|
17
27
|
*/
|
|
18
|
-
constructor(
|
|
28
|
+
constructor(pFable)
|
|
19
29
|
{
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
// Ensure the cache directory exists
|
|
23
|
-
if (!libFs.existsSync(this._cachePath))
|
|
24
|
-
{
|
|
25
|
-
libFs.mkdirSync(this._cachePath, { recursive: true });
|
|
26
|
-
}
|
|
30
|
+
this._fable = pFable;
|
|
31
|
+
this._storage = pFable.ParimeBinaryStorage;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* Build a cache key from the source file path, its mtime, and the
|
|
31
36
|
* requested thumbnail dimensions.
|
|
32
37
|
*
|
|
38
|
+
* The key has two parts separated by a slash:
|
|
39
|
+
* {folderHash}/{fileHash}
|
|
40
|
+
*
|
|
41
|
+
* The folder hash groups all thumbnails from the same directory
|
|
42
|
+
* together so they land in the same shard directory on disk.
|
|
43
|
+
* The file hash uniquely identifies a particular file at a
|
|
44
|
+
* particular mtime and dimension.
|
|
45
|
+
*
|
|
33
46
|
* @param {string} pFilePath - Relative path to the source file
|
|
34
47
|
* @param {number} pMtime - Source file mtime (ms since epoch)
|
|
35
48
|
* @param {number} pWidth - Thumbnail width
|
|
36
49
|
* @param {number} pHeight - Thumbnail height
|
|
37
|
-
* @returns {string} A
|
|
50
|
+
* @returns {string} A composite key "{folderHash}/{fileHash}"
|
|
38
51
|
*/
|
|
39
52
|
buildKey(pFilePath, pMtime, pWidth, pHeight)
|
|
40
53
|
{
|
|
41
|
-
let
|
|
42
|
-
|
|
54
|
+
let tmpDir = libPath.dirname(pFilePath);
|
|
55
|
+
let tmpFile = libPath.basename(pFilePath);
|
|
56
|
+
|
|
57
|
+
// Folder hash — first 16 hex chars; used for sharding and co-location
|
|
58
|
+
let tmpFolderHash = libCrypto.createHash('sha256').update(tmpDir).digest('hex').substring(0, 16);
|
|
59
|
+
|
|
60
|
+
// File-specific hash — full 64 hex chars; unique per file + dimensions
|
|
61
|
+
let tmpFileInput = `${tmpFile}:${pMtime}:${pWidth}x${pHeight}`;
|
|
62
|
+
let tmpFileHash = libCrypto.createHash('sha256').update(tmpFileInput).digest('hex');
|
|
63
|
+
|
|
64
|
+
return `${tmpFolderHash}/${tmpFileHash}`;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
/**
|
|
@@ -52,7 +74,8 @@ class ThumbnailCache
|
|
|
52
74
|
*/
|
|
53
75
|
get(pKey, pFormat)
|
|
54
76
|
{
|
|
55
|
-
let
|
|
77
|
+
let tmpFileKey = `${pKey}.${pFormat || 'webp'}`;
|
|
78
|
+
let tmpPath = this._storage.resolvePath(_CATEGORY, tmpFileKey);
|
|
56
79
|
if (libFs.existsSync(tmpPath))
|
|
57
80
|
{
|
|
58
81
|
return tmpPath;
|
|
@@ -70,19 +93,28 @@ class ThumbnailCache
|
|
|
70
93
|
*/
|
|
71
94
|
put(pKey, pBuffer, pFormat)
|
|
72
95
|
{
|
|
73
|
-
let
|
|
96
|
+
let tmpFileKey = `${pKey}.${pFormat || 'webp'}`;
|
|
97
|
+
let tmpPath = this._storage.resolvePath(_CATEGORY, tmpFileKey);
|
|
98
|
+
|
|
99
|
+
// Ensure parent directory exists (for sharded paths)
|
|
100
|
+
let tmpDir = libPath.dirname(tmpPath);
|
|
101
|
+
if (!libFs.existsSync(tmpDir))
|
|
102
|
+
{
|
|
103
|
+
libFs.mkdirSync(tmpDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
libFs.writeFileSync(tmpPath, pBuffer);
|
|
75
107
|
return tmpPath;
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
/**
|
|
79
|
-
* Get the
|
|
111
|
+
* Get the cache category name.
|
|
80
112
|
*
|
|
81
113
|
* @returns {string}
|
|
82
114
|
*/
|
|
83
115
|
getCachePath()
|
|
84
116
|
{
|
|
85
|
-
return this.
|
|
117
|
+
return this._storage.resolvePath(_CATEGORY, '');
|
|
86
118
|
}
|
|
87
119
|
}
|
|
88
120
|
|
|
@@ -47,16 +47,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
47
47
|
|
|
48
48
|
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
49
49
|
|
|
50
|
-
this.
|
|
51
|
-
|| libPath.join(process.cwd(), 'dist', 'retold-cache', 'video-frames');
|
|
52
|
-
|
|
53
|
-
// Ensure cache directory exists
|
|
54
|
-
if (!libFs.existsSync(this.cachePath))
|
|
55
|
-
{
|
|
56
|
-
libFs.mkdirSync(this.cachePath, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this.fable.log.info(`Video Frame Service: cache at ${this.cachePath}`);
|
|
50
|
+
this.fable.log.info('Video Frame Service: using ParimeBinaryStorage (category: video-frames)');
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
/**
|
|
@@ -75,7 +66,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
75
66
|
{
|
|
76
67
|
let tmpInput = `${pAbsPath}:${pMtimeMs}:${pFrameCount}:${pWidth}x${pHeight}`;
|
|
77
68
|
let tmpHash = libCrypto.createHash('sha256').update(tmpInput).digest('hex').substring(0, 16);
|
|
78
|
-
return
|
|
69
|
+
return this.fable.ParimeBinaryStorage.resolvePath('video-frames', tmpHash);
|
|
79
70
|
}
|
|
80
71
|
|
|
81
72
|
/**
|
|
@@ -398,7 +389,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
398
389
|
return fCallback(new Error('Invalid cache key.'));
|
|
399
390
|
}
|
|
400
391
|
|
|
401
|
-
let tmpCacheDir =
|
|
392
|
+
let tmpCacheDir = this.fable.ParimeBinaryStorage.resolvePath('video-frames', pCacheKey);
|
|
402
393
|
if (!libFs.existsSync(tmpCacheDir))
|
|
403
394
|
{
|
|
404
395
|
return fCallback(new Error('Cache directory not found. Extract frames first.'));
|
|
@@ -466,11 +457,12 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
|
|
|
466
457
|
return null;
|
|
467
458
|
}
|
|
468
459
|
|
|
469
|
-
let
|
|
460
|
+
let tmpCacheDir = this.fable.ParimeBinaryStorage.resolvePath('video-frames', pCacheKey);
|
|
461
|
+
let tmpPath = libPath.join(tmpCacheDir, pFilename);
|
|
470
462
|
|
|
471
|
-
// Double-check it's under
|
|
463
|
+
// Double-check it's under the storage root
|
|
472
464
|
let tmpResolved = libPath.resolve(tmpPath);
|
|
473
|
-
if (!tmpResolved.startsWith(this.
|
|
465
|
+
if (!tmpResolved.startsWith(this.fable.ParimeBinaryStorage.storageRoot))
|
|
474
466
|
{
|
|
475
467
|
return null;
|
|
476
468
|
}
|