retold-remote 0.0.1
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/LICENSE +21 -0
- package/css/retold-remote.css +83 -0
- package/html/codejar.js +511 -0
- package/html/index.html +23 -0
- package/package.json +68 -0
- package/server.js +43 -0
- package/source/Pict-Application-RetoldRemote-Configuration.json +7 -0
- package/source/Pict-Application-RetoldRemote.js +622 -0
- package/source/Pict-RetoldRemote-Bundle.js +14 -0
- package/source/cli/RetoldRemote-CLI-Program.js +15 -0
- package/source/cli/RetoldRemote-CLI-Run.js +3 -0
- package/source/cli/RetoldRemote-Server-Setup.js +257 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +87 -0
- package/source/providers/Pict-Provider-GalleryFilterSort.js +597 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +819 -0
- package/source/providers/Pict-Provider-RetoldRemote.js +273 -0
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +640 -0
- package/source/providers/Pict-Provider-RetoldRemoteTheme.js +879 -0
- package/source/server/RetoldRemote-MediaService.js +536 -0
- package/source/server/RetoldRemote-PathRegistry.js +121 -0
- package/source/server/RetoldRemote-ThumbnailCache.js +89 -0
- package/source/server/RetoldRemote-ToolDetector.js +78 -0
- package/source/views/PictView-Remote-Gallery.js +1437 -0
- package/source/views/PictView-Remote-ImageViewer.js +363 -0
- package/source/views/PictView-Remote-Layout.js +420 -0
- package/source/views/PictView-Remote-MediaViewer.js +530 -0
- package/source/views/PictView-Remote-SettingsPanel.js +318 -0
- package/source/views/PictView-Remote-TopBar.js +206 -0
- package/web-application/codejar.js +511 -0
- package/web-application/css/retold-remote.css +83 -0
- package/web-application/index.html +23 -0
- package/web-application/js/pict.min.js +12 -0
- package/web-application/js/pict.min.js.map +1 -0
- package/web-application/retold-remote.compatible.js +5764 -0
- package/web-application/retold-remote.compatible.js.map +1 -0
- package/web-application/retold-remote.compatible.min.js +120 -0
- package/web-application/retold-remote.compatible.min.js.map +1 -0
- package/web-application/retold-remote.js +5763 -0
- package/web-application/retold-remote.js.map +1 -0
- package/web-application/retold-remote.min.js +120 -0
- package/web-application/retold-remote.min.js.map +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const libCLIProgram = require('pict-service-commandlineutility');
|
|
2
|
+
|
|
3
|
+
let _PictCLIProgram = new libCLIProgram(
|
|
4
|
+
{
|
|
5
|
+
Product: 'retold-remote',
|
|
6
|
+
Version: require('../../package.json').version,
|
|
7
|
+
|
|
8
|
+
Command: 'retold-remote',
|
|
9
|
+
Description: 'Browse and view media files on a NAS or local folder with gallery views and keyboard navigation.'
|
|
10
|
+
},
|
|
11
|
+
[
|
|
12
|
+
require('./commands/RetoldRemote-Command-Serve.js')
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
module.exports = _PictCLIProgram;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- Orator Server Setup
|
|
3
|
+
*
|
|
4
|
+
* Composes an Orator server with:
|
|
5
|
+
* 1. pict-section-filebrowser service for directory browsing
|
|
6
|
+
* 2. Content static serving at /content/*
|
|
7
|
+
* 3. Media service (thumbnails, probing, folder summaries)
|
|
8
|
+
* 4. Static serving of the retold-remote web application
|
|
9
|
+
*
|
|
10
|
+
* This is intentionally a standalone server setup (not calling content-system's
|
|
11
|
+
* server setup) so that we control all routes and avoid conflicts with editor-
|
|
12
|
+
* specific endpoints (save, upload) that aren't needed here.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} pOptions
|
|
15
|
+
* @param {string} pOptions.ContentPath - Absolute path to the media folder to browse
|
|
16
|
+
* @param {string} pOptions.DistPath - Absolute path to the built web-application folder
|
|
17
|
+
* @param {number} pOptions.Port - HTTP port
|
|
18
|
+
* @param {string} [pOptions.ThumbnailCachePath] - Override thumbnail cache location
|
|
19
|
+
* @param {boolean} [pOptions.HashedFilenames] - Enable hashed filenames mode
|
|
20
|
+
* @param {Function} fCallback - Callback(pError, { Fable, Orator, Port })
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const libFs = require('fs');
|
|
24
|
+
const libPath = require('path');
|
|
25
|
+
|
|
26
|
+
const libFable = require('fable');
|
|
27
|
+
const libOrator = require('orator');
|
|
28
|
+
const libOratorServiceServerRestify = require('orator-serviceserver-restify');
|
|
29
|
+
const libFileBrowserService = require('pict-section-filebrowser').FileBrowserService;
|
|
30
|
+
|
|
31
|
+
const libRetoldRemoteMediaService = require('../server/RetoldRemote-MediaService.js');
|
|
32
|
+
const libRetoldRemotePathRegistry = require('../server/RetoldRemote-PathRegistry.js');
|
|
33
|
+
|
|
34
|
+
function setupRetoldRemoteServer(pOptions, fCallback)
|
|
35
|
+
{
|
|
36
|
+
let tmpContentPath = pOptions.ContentPath;
|
|
37
|
+
let tmpDistFolder = pOptions.DistPath;
|
|
38
|
+
let tmpPort = pOptions.Port;
|
|
39
|
+
let tmpHashedFilenames = !!(pOptions.HashedFilenames || process.env.RETOLD_HASHED_FILENAMES === 'true');
|
|
40
|
+
|
|
41
|
+
let tmpSettings =
|
|
42
|
+
{
|
|
43
|
+
Product: 'Retold-Remote',
|
|
44
|
+
ProductVersion: require('../../package.json').version,
|
|
45
|
+
APIServerPort: tmpPort,
|
|
46
|
+
ContentPath: tmpContentPath
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let tmpFable = new libFable(tmpSettings);
|
|
50
|
+
|
|
51
|
+
// Ensure the content directory exists
|
|
52
|
+
if (!libFs.existsSync(tmpContentPath))
|
|
53
|
+
{
|
|
54
|
+
libFs.mkdirSync(tmpContentPath, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tmpFable.serviceManager.addServiceType('OratorServiceServer', libOratorServiceServerRestify);
|
|
58
|
+
tmpFable.serviceManager.instantiateServiceProvider('OratorServiceServer');
|
|
59
|
+
tmpFable.serviceManager.addServiceType('Orator', libOrator);
|
|
60
|
+
let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator');
|
|
61
|
+
|
|
62
|
+
// Set up the file browser service
|
|
63
|
+
let tmpFileBrowser = new libFileBrowserService(tmpFable,
|
|
64
|
+
{
|
|
65
|
+
BasePath: tmpContentPath,
|
|
66
|
+
APIRoutePrefix: '/api/filebrowser',
|
|
67
|
+
ServeWebApp: false,
|
|
68
|
+
IncludeHiddenFiles: false
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Set up the path registry (for hashed filenames)
|
|
72
|
+
let tmpPathRegistry = new libRetoldRemotePathRegistry(tmpFable,
|
|
73
|
+
{
|
|
74
|
+
Enabled: tmpHashedFilenames
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (tmpHashedFilenames)
|
|
78
|
+
{
|
|
79
|
+
tmpFable.log.info('Hashed filenames mode: ENABLED');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Set up the media service
|
|
83
|
+
let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
|
|
84
|
+
{
|
|
85
|
+
ContentPath: tmpContentPath,
|
|
86
|
+
ThumbnailCachePath: pOptions.ThumbnailCachePath || null,
|
|
87
|
+
APIRoutePrefix: '/api/media',
|
|
88
|
+
PathRegistry: tmpPathRegistry
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
tmpOrator.initialize(
|
|
92
|
+
function ()
|
|
93
|
+
{
|
|
94
|
+
let tmpServiceServer = tmpOrator.serviceServer;
|
|
95
|
+
|
|
96
|
+
// Enable body parsing
|
|
97
|
+
tmpServiceServer.server.use(tmpServiceServer.bodyParser());
|
|
98
|
+
|
|
99
|
+
// Hash resolution middleware: if hashed filenames is enabled,
|
|
100
|
+
// intercept ?path= query params and resolve hashes to paths.
|
|
101
|
+
if (tmpHashedFilenames)
|
|
102
|
+
{
|
|
103
|
+
let libUrlMiddleware = require('url');
|
|
104
|
+
tmpServiceServer.server.use(
|
|
105
|
+
(pRequest, pResponse, fNext) =>
|
|
106
|
+
{
|
|
107
|
+
let tmpParsedUrl = libUrlMiddleware.parse(pRequest.url, true);
|
|
108
|
+
let tmpPathParam = tmpParsedUrl.query && tmpParsedUrl.query.path;
|
|
109
|
+
if (tmpPathParam && /^[a-f0-9]{10}$/.test(tmpPathParam))
|
|
110
|
+
{
|
|
111
|
+
let tmpResolved = tmpPathRegistry.resolve(tmpPathParam);
|
|
112
|
+
if (tmpResolved !== null)
|
|
113
|
+
{
|
|
114
|
+
// Replace the hash with the resolved path in the query
|
|
115
|
+
tmpParsedUrl.query.path = tmpResolved;
|
|
116
|
+
delete tmpParsedUrl.search;
|
|
117
|
+
pRequest.url = libUrlMiddleware.format(tmpParsedUrl);
|
|
118
|
+
if (pRequest.query)
|
|
119
|
+
{
|
|
120
|
+
pRequest.query.path = tmpResolved;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return fNext();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- GET /api/remote/settings ---
|
|
129
|
+
// Returns server settings so the client knows if hashed filenames is on.
|
|
130
|
+
tmpServiceServer.get('/api/remote/settings',
|
|
131
|
+
(pRequest, pResponse, fNext) =>
|
|
132
|
+
{
|
|
133
|
+
pResponse.send(
|
|
134
|
+
{
|
|
135
|
+
Success: true,
|
|
136
|
+
HashedFilenames: tmpHashedFilenames
|
|
137
|
+
});
|
|
138
|
+
return fNext();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Response annotation middleware: when hashed filenames is enabled,
|
|
142
|
+
// wrap res.send for /api/filebrowser/list to annotate entries with hashes.
|
|
143
|
+
if (tmpHashedFilenames)
|
|
144
|
+
{
|
|
145
|
+
let libUrlListWrap = require('url');
|
|
146
|
+
tmpServiceServer.server.use(
|
|
147
|
+
(pRequest, pResponse, fNext) =>
|
|
148
|
+
{
|
|
149
|
+
if (!pRequest.url.startsWith('/api/filebrowser/list'))
|
|
150
|
+
{
|
|
151
|
+
return fNext();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let tmpOriginalSend = pResponse.send.bind(pResponse);
|
|
155
|
+
pResponse.send = function (pStatusOrData, pData)
|
|
156
|
+
{
|
|
157
|
+
let tmpFileList = (typeof (pStatusOrData) === 'number') ? pData : pStatusOrData;
|
|
158
|
+
|
|
159
|
+
if (Array.isArray(tmpFileList))
|
|
160
|
+
{
|
|
161
|
+
// Annotate entries with hashes
|
|
162
|
+
tmpPathRegistry.annotateFileList(tmpFileList);
|
|
163
|
+
|
|
164
|
+
// Register the current folder path and set header
|
|
165
|
+
let tmpParsedListUrl = libUrlListWrap.parse(pRequest.url, true);
|
|
166
|
+
let tmpFolderPath = (tmpParsedListUrl.query && tmpParsedListUrl.query.path) || '';
|
|
167
|
+
if (tmpFolderPath)
|
|
168
|
+
{
|
|
169
|
+
let tmpFolderHash = tmpPathRegistry.register(tmpFolderPath);
|
|
170
|
+
pResponse.header('X-Retold-Folder-Hash', tmpFolderHash);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return tmpOriginalSend(pStatusOrData, pData);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return fNext();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Connect file browser API routes
|
|
182
|
+
tmpFileBrowser.connectRoutes();
|
|
183
|
+
|
|
184
|
+
// PUT /api/filebrowser/settings -- toggle hidden files at runtime
|
|
185
|
+
tmpServiceServer.put('/api/filebrowser/settings',
|
|
186
|
+
(pRequest, pResponse, fNext) =>
|
|
187
|
+
{
|
|
188
|
+
try
|
|
189
|
+
{
|
|
190
|
+
if (pRequest.body && typeof (pRequest.body.IncludeHiddenFiles) === 'boolean')
|
|
191
|
+
{
|
|
192
|
+
tmpFileBrowser.options.IncludeHiddenFiles = pRequest.body.IncludeHiddenFiles;
|
|
193
|
+
}
|
|
194
|
+
pResponse.send({ Success: true });
|
|
195
|
+
}
|
|
196
|
+
catch (pError)
|
|
197
|
+
{
|
|
198
|
+
pResponse.send(500, { Error: pError.message });
|
|
199
|
+
}
|
|
200
|
+
return fNext();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Connect media service API routes
|
|
204
|
+
tmpMediaService.connectRoutes(tmpServiceServer);
|
|
205
|
+
|
|
206
|
+
// Content-hashed URL rewrite: resolve /content-hashed/<hash>
|
|
207
|
+
// to /content/<resolved-path> so the static route serves the file.
|
|
208
|
+
// Uses server.pre() to rewrite BEFORE route matching.
|
|
209
|
+
if (tmpHashedFilenames)
|
|
210
|
+
{
|
|
211
|
+
tmpServiceServer.server.pre(
|
|
212
|
+
(pRequest, pResponse, fNext) =>
|
|
213
|
+
{
|
|
214
|
+
let tmpMatch = /^\/content-hashed\/([a-f0-9]{10})$/.exec(pRequest.url);
|
|
215
|
+
if (!tmpMatch)
|
|
216
|
+
{
|
|
217
|
+
return fNext();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let tmpHash = tmpMatch[1];
|
|
221
|
+
let tmpResolvedPath = tmpPathRegistry.resolve(tmpHash);
|
|
222
|
+
|
|
223
|
+
if (!tmpResolvedPath)
|
|
224
|
+
{
|
|
225
|
+
pResponse.send(404, { Error: 'Unknown hash.' });
|
|
226
|
+
return fNext(false);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Rewrite URL to /content/<path> before route matching
|
|
230
|
+
pRequest.url = '/content/' + tmpResolvedPath;
|
|
231
|
+
return fNext();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Serve content files at /content/ (for direct media access)
|
|
236
|
+
tmpOrator.addStaticRoute(`${tmpContentPath}/`, 'index.html', '/content/*', '/content/');
|
|
237
|
+
|
|
238
|
+
// Serve the built web application (main static route)
|
|
239
|
+
tmpOrator.addStaticRoute(`${tmpDistFolder}/`, 'index.html');
|
|
240
|
+
|
|
241
|
+
// Start the server
|
|
242
|
+
tmpOrator.startService(
|
|
243
|
+
function ()
|
|
244
|
+
{
|
|
245
|
+
return fCallback(null,
|
|
246
|
+
{
|
|
247
|
+
Fable: tmpFable,
|
|
248
|
+
Orator: tmpOrator,
|
|
249
|
+
MediaService: tmpMediaService,
|
|
250
|
+
PathRegistry: tmpPathRegistry,
|
|
251
|
+
Port: tmpPort
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = setupRetoldRemoteServer;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
|
|
2
|
+
|
|
3
|
+
const libFs = require('fs');
|
|
4
|
+
const libPath = require('path');
|
|
5
|
+
|
|
6
|
+
class RetoldRemoteCommandServe extends libCommandLineCommand
|
|
7
|
+
{
|
|
8
|
+
constructor(pFable, pManifest, pServiceHash)
|
|
9
|
+
{
|
|
10
|
+
super(pFable, pManifest, pServiceHash);
|
|
11
|
+
|
|
12
|
+
this.options.CommandKeyword = 'serve';
|
|
13
|
+
this.options.Description = 'Start the Retold Remote media browser for a folder.';
|
|
14
|
+
|
|
15
|
+
this.options.CommandArguments.push(
|
|
16
|
+
{ Name: '[content-path]', Description: 'Path to the media folder to browse (defaults to current directory).' });
|
|
17
|
+
|
|
18
|
+
this.options.CommandOptions.push(
|
|
19
|
+
{ Name: '-p, --port [port]', Description: 'Port to serve on (defaults to random 7000-7999).', Default: '0' });
|
|
20
|
+
|
|
21
|
+
this.options.CommandOptions.push(
|
|
22
|
+
{ Name: '-H, --hashed-filenames', Description: 'Enable hashed filenames mode (short hashes instead of full paths in URLs).', Default: false });
|
|
23
|
+
|
|
24
|
+
this.addCommand();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onRunAsync(fCallback)
|
|
28
|
+
{
|
|
29
|
+
let tmpContentPath = libPath.resolve(this.ArgumentString || process.cwd());
|
|
30
|
+
|
|
31
|
+
let tmpDistPath = libPath.resolve(__dirname, '..', '..', '..', 'web-application');
|
|
32
|
+
let tmpPortOption = parseInt(this.CommandOptions.port, 10);
|
|
33
|
+
let tmpPort = (tmpPortOption > 0) ? tmpPortOption : (7000 + Math.floor(Math.random() * 1000));
|
|
34
|
+
|
|
35
|
+
// Validate web-application path
|
|
36
|
+
if (!libFs.existsSync(tmpDistPath))
|
|
37
|
+
{
|
|
38
|
+
this.log.error(`Built assets not found at ${tmpDistPath}. Run 'npm run build' in the retold-remote package first.`);
|
|
39
|
+
return fCallback(new Error('web-application folder not found'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Ensure content directory exists
|
|
43
|
+
if (!libFs.existsSync(tmpContentPath))
|
|
44
|
+
{
|
|
45
|
+
libFs.mkdirSync(tmpContentPath, { recursive: true });
|
|
46
|
+
this.log.info(`Created content directory: ${tmpContentPath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let tmpSelf = this;
|
|
50
|
+
let tmpSetupServer = require('../RetoldRemote-Server-Setup.js');
|
|
51
|
+
|
|
52
|
+
let tmpHashedFilenames = !!(this.CommandOptions.hashedFilenames);
|
|
53
|
+
|
|
54
|
+
tmpSetupServer(
|
|
55
|
+
{
|
|
56
|
+
ContentPath: tmpContentPath,
|
|
57
|
+
DistPath: tmpDistPath,
|
|
58
|
+
Port: tmpPort,
|
|
59
|
+
HashedFilenames: tmpHashedFilenames
|
|
60
|
+
},
|
|
61
|
+
function (pError, pServerInfo)
|
|
62
|
+
{
|
|
63
|
+
if (pError)
|
|
64
|
+
{
|
|
65
|
+
tmpSelf.log.error(`Failed to start server: ${pError.message}`);
|
|
66
|
+
return fCallback(pError);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
tmpSelf.log.info('');
|
|
70
|
+
tmpSelf.log.info('==========================================================');
|
|
71
|
+
tmpSelf.log.info(` Retold Remote running on http://localhost:${pServerInfo.Port}`);
|
|
72
|
+
tmpSelf.log.info('==========================================================');
|
|
73
|
+
tmpSelf.log.info(` Content: ${tmpContentPath}`);
|
|
74
|
+
tmpSelf.log.info(` Assets: ${tmpDistPath}`);
|
|
75
|
+
tmpSelf.log.info(` Browse: http://localhost:${pServerInfo.Port}/`);
|
|
76
|
+
tmpSelf.log.info('==========================================================');
|
|
77
|
+
tmpSelf.log.info('');
|
|
78
|
+
tmpSelf.log.info(' Press Ctrl+C to stop.');
|
|
79
|
+
tmpSelf.log.info('');
|
|
80
|
+
|
|
81
|
+
// Intentionally do NOT call fCallback() here.
|
|
82
|
+
// The server should keep running.
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = RetoldRemoteCommandServe;
|