@saltcorn/filemanager 0.8.0-beta.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.
@@ -0,0 +1,76 @@
1
+ import svelte from 'rollup-plugin-svelte';
2
+ import commonjs from '@rollup/plugin-commonjs';
3
+ import resolve from '@rollup/plugin-node-resolve';
4
+ import livereload from 'rollup-plugin-livereload';
5
+ import { terser } from 'rollup-plugin-terser';
6
+ import css from 'rollup-plugin-css-only';
7
+
8
+ const production = !process.env.ROLLUP_WATCH;
9
+
10
+ function serve() {
11
+ let server;
12
+
13
+ function toExit() {
14
+ if (server) server.kill(0);
15
+ }
16
+
17
+ return {
18
+ writeBundle() {
19
+ if (server) return;
20
+ server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
21
+ stdio: ['ignore', 'inherit', 'inherit'],
22
+ shell: true
23
+ });
24
+
25
+ process.on('SIGTERM', toExit);
26
+ process.on('exit', toExit);
27
+ }
28
+ };
29
+ }
30
+
31
+ export default {
32
+ input: 'src/main.js',
33
+ output: {
34
+ sourcemap: true,
35
+ format: 'iife',
36
+ name: 'app',
37
+ file: 'public/build/bundle.js'
38
+ },
39
+ plugins: [
40
+ svelte({
41
+ compilerOptions: {
42
+ // enable run-time checks when not in production
43
+ dev: !production
44
+ }
45
+ }),
46
+ // we'll extract any component CSS out into
47
+ // a separate file - better for performance
48
+ css({ output: 'bundle.css' }),
49
+
50
+ // If you have external dependencies installed from
51
+ // npm, you'll most likely need these plugins. In
52
+ // some cases you'll need additional configuration -
53
+ // consult the documentation for details:
54
+ // https://github.com/rollup/plugins/tree/master/packages/commonjs
55
+ resolve({
56
+ browser: true,
57
+ dedupe: ['svelte']
58
+ }),
59
+ commonjs(),
60
+
61
+ // In dev mode, call `npm run start` once
62
+ // the bundle has been generated
63
+ !production && serve(),
64
+
65
+ // Watch the `public` directory and refresh the
66
+ // browser on changes when not in production
67
+ !production && livereload('public'),
68
+
69
+ // If we're building for production (npm run build
70
+ // instead of npm run dev), minify
71
+ production && terser()
72
+ ],
73
+ watch: {
74
+ clearScreen: false
75
+ }
76
+ };
@@ -0,0 +1,121 @@
1
+ // @ts-check
2
+
3
+ /** This script modifies the project to support TS code in .svelte files like:
4
+
5
+ <script lang="ts">
6
+ export let name: string;
7
+ </script>
8
+
9
+ As well as validating the code for CI.
10
+ */
11
+
12
+ /** To work on this script:
13
+ rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
14
+ */
15
+
16
+ const fs = require("fs")
17
+ const path = require("path")
18
+ const { argv } = require("process")
19
+
20
+ const projectRoot = argv[2] || path.join(__dirname, "..")
21
+
22
+ // Add deps to pkg.json
23
+ const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
24
+ packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
25
+ "svelte-check": "^2.0.0",
26
+ "svelte-preprocess": "^4.0.0",
27
+ "@rollup/plugin-typescript": "^8.0.0",
28
+ "typescript": "^4.0.0",
29
+ "tslib": "^2.0.0",
30
+ "@tsconfig/svelte": "^2.0.0"
31
+ })
32
+
33
+ // Add script for checking
34
+ packageJSON.scripts = Object.assign(packageJSON.scripts, {
35
+ "check": "svelte-check --tsconfig ./tsconfig.json"
36
+ })
37
+
38
+ // Write the package JSON
39
+ fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
40
+
41
+ // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
42
+ const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
43
+ const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
44
+ fs.renameSync(beforeMainJSPath, afterMainTSPath)
45
+
46
+ // Switch the app.svelte file to use TS
47
+ const appSveltePath = path.join(projectRoot, "src", "App.svelte")
48
+ let appFile = fs.readFileSync(appSveltePath, "utf8")
49
+ appFile = appFile.replace("<script>", '<script lang="ts">')
50
+ appFile = appFile.replace("export let name;", 'export let name: string;')
51
+ fs.writeFileSync(appSveltePath, appFile)
52
+
53
+ // Edit rollup config
54
+ const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
55
+ let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
56
+
57
+ // Edit imports
58
+ rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
59
+ import sveltePreprocess from 'svelte-preprocess';
60
+ import typescript from '@rollup/plugin-typescript';`)
61
+
62
+ // Replace name of entry point
63
+ rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
64
+
65
+ // Add preprocessor
66
+ rollupConfig = rollupConfig.replace(
67
+ 'compilerOptions:',
68
+ 'preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
69
+ );
70
+
71
+ // Add TypeScript
72
+ rollupConfig = rollupConfig.replace(
73
+ 'commonjs(),',
74
+ 'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
75
+ );
76
+ fs.writeFileSync(rollupConfigPath, rollupConfig)
77
+
78
+ // Add TSConfig
79
+ const tsconfig = `{
80
+ "extends": "@tsconfig/svelte/tsconfig.json",
81
+
82
+ "include": ["src/**/*"],
83
+ "exclude": ["node_modules/*", "__sapper__/*", "public/*"]
84
+ }`
85
+ const tsconfigPath = path.join(projectRoot, "tsconfig.json")
86
+ fs.writeFileSync(tsconfigPath, tsconfig)
87
+
88
+ // Add global.d.ts
89
+ const dtsPath = path.join(projectRoot, "src", "global.d.ts")
90
+ fs.writeFileSync(dtsPath, `/// <reference types="svelte" />`)
91
+
92
+ // Delete this script, but not during testing
93
+ if (!argv[2]) {
94
+ // Remove the script
95
+ fs.unlinkSync(path.join(__filename))
96
+
97
+ // Check for Mac's DS_store file, and if it's the only one left remove it
98
+ const remainingFiles = fs.readdirSync(path.join(__dirname))
99
+ if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
100
+ fs.unlinkSync(path.join(__dirname, '.DS_store'))
101
+ }
102
+
103
+ // Check if the scripts folder is empty
104
+ if (fs.readdirSync(path.join(__dirname)).length === 0) {
105
+ // Remove the scripts folder
106
+ fs.rmdirSync(path.join(__dirname))
107
+ }
108
+ }
109
+
110
+ // Adds the extension recommendation
111
+ fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
112
+ fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
113
+ "recommendations": ["svelte.svelte-vscode"]
114
+ }
115
+ `)
116
+
117
+ console.log("Converted to TypeScript.")
118
+
119
+ if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
120
+ console.log("\nYou will need to re-run your dependency manager to get started.")
121
+ }
package/src/App.svelte ADDED
@@ -0,0 +1,387 @@
1
+ <script>
2
+ import { onMount } from "svelte";
3
+ import Fa from "svelte-fa";
4
+ import {
5
+ faTrashAlt,
6
+ faFileImage,
7
+ faFile,
8
+ faFolder,
9
+ faFileCsv,
10
+ faFileExcel,
11
+ faFileWord,
12
+ faFilePdf,
13
+ faFileAlt,
14
+ faFileAudio,
15
+ faFileVideo,
16
+ faFolderPlus,
17
+ faHome,
18
+ faCaretUp,
19
+ faCaretDown,
20
+ } from "@fortawesome/free-solid-svg-icons";
21
+ export let files = [];
22
+ export let directories = [];
23
+ export let roles = {};
24
+ export let currentFolder = "/";
25
+ let selectedList = [];
26
+ let selectedFiles = {};
27
+ let rolesList;
28
+ let lastSelected;
29
+ const fetchAndReset = async function (keepSelection) {
30
+ const response = await fetch(`/files?dir=${currentFolder}`, {
31
+ headers: { "X-Requested-With": "XMLHttpRequest" },
32
+ });
33
+ const data = await response.json();
34
+ files = data.files;
35
+ for (const file of files) {
36
+ file.mimetype =
37
+ file.mime_sub && file.mime_super
38
+ ? `${file.mime_super}/${file.mime_sub}`
39
+ : "";
40
+ }
41
+ directories = data.directories;
42
+ rolesList = data.roles;
43
+ for (const role of data.roles) {
44
+ roles[role.id] = role.role;
45
+ }
46
+ if (!keepSelection) {
47
+ selectedList = [];
48
+ selectedFiles = {};
49
+ lastSelected = null;
50
+ } else if (lastSelected) {
51
+ lastSelected = files.find((f) => f.filename === lastSelected.filename);
52
+ }
53
+ clickHeader("filename");
54
+ };
55
+ onMount(fetchAndReset);
56
+ function rowClick(file, e) {
57
+ file.selected = true;
58
+ const prev = selectedFiles[file.filename];
59
+ if (!e.shiftKey) selectedFiles = {};
60
+ selectedFiles[file.filename] = !prev;
61
+ if (!prev) lastSelected = file;
62
+ else {
63
+ const firstSelected = Object.entries(selectedFiles).findLast(
64
+ ([k, v]) => v
65
+ );
66
+ if (firstSelected)
67
+ lastSelected = files.find((f) => f.filename === firstSelected[0]);
68
+ else lastSelected = null;
69
+ }
70
+ document.getSelection().removeAllRanges();
71
+ console.log(lastSelected);
72
+ }
73
+ $: selectedList = Object.entries(selectedFiles)
74
+ .filter(([k, v]) => v)
75
+ .map(([k, v]) => k);
76
+
77
+ async function POST(url, body) {
78
+ return await fetch(url, {
79
+ headers: {
80
+ "X-Requested-With": "XMLHttpRequest",
81
+ "CSRF-Token": window._sc_globalCsrf,
82
+ "Content-Type": "application/json",
83
+ },
84
+ method: "POST",
85
+ body: JSON.stringify(body || {}),
86
+ });
87
+ }
88
+
89
+ async function goAction(e) {
90
+ const action = e?.target.value;
91
+ if (!action) return;
92
+ switch (action) {
93
+ case "Delete":
94
+ if (!confirm(`Delete files: ${selectedList.join()}`)) return;
95
+ for (const fileNm of selectedList) {
96
+ const file = files.find((f) => f.filename === fileNm);
97
+ await POST(`/files/delete/${file.location}`);
98
+ }
99
+ await fetchAndReset();
100
+ break;
101
+ case "Rename":
102
+ const newName = window.prompt(
103
+ `Rename ${lastSelected.filename} to:`,
104
+ lastSelected.filename
105
+ );
106
+ if (!newName) return;
107
+ await POST(`/files/setname/${lastSelected.location}`, {
108
+ value: newName,
109
+ });
110
+ await fetchAndReset();
111
+ break;
112
+ }
113
+ }
114
+ async function changeAccessRole(e) {
115
+ const role = e.target.value;
116
+ for (const fileNm of selectedList) {
117
+ const file = files.find((f) => f.filename === fileNm);
118
+ await POST(`/files/setrole/${file.location}`, { role });
119
+ }
120
+ await fetchAndReset(true);
121
+ }
122
+ async function moveDirectory(e) {
123
+ for (const fileNm of selectedList) {
124
+ const new_path = e.target.value;
125
+ if (!new_path) return;
126
+ const file = files.find((f) => f.filename === fileNm);
127
+ await POST(`/files/move/${file.location}`, { new_path });
128
+ }
129
+ await fetchAndReset();
130
+ }
131
+
132
+ function gotoFolder(folder) {
133
+ currentFolder = folder;
134
+ fetchAndReset();
135
+ }
136
+
137
+ let pathSegments = [];
138
+ $: {
139
+ if (currentFolder === "/" || currentFolder === "")
140
+ pathSegments = [{ icon: faHome, location: "/" }];
141
+ else {
142
+ pathSegments = currentFolder.split("/").map((name, i) => ({
143
+ name,
144
+ location: currentFolder
145
+ .split("/")
146
+ .slice(0, i + 1)
147
+ .join("/"),
148
+ }));
149
+ pathSegments.unshift({ icon: faHome, location: "/" });
150
+ }
151
+ }
152
+
153
+ function getIcon(file) {
154
+ if (file.mime_super === "image") return faFileImage;
155
+ if (file.mime_super === "audio") return faFileAudio;
156
+ if (file.mime_super === "video") return faFileVideo;
157
+ if (file.mime_sub === "pdf") return faFilePdf;
158
+
159
+ if (file.isDirectory) return faFolder;
160
+ const fname = file.filename.toLowerCase();
161
+ if (fname.endsWith(".csv")) return faFileCsv;
162
+ if (fname.endsWith(".xls")) return faFileExcel;
163
+ if (fname.endsWith(".xlsx")) return faFileExcel;
164
+ if (fname.endsWith(".doc")) return faFileWord;
165
+ if (fname.endsWith(".docx")) return faFileWord;
166
+ if (fname.endsWith(".txt")) return faFileAlt;
167
+ return faFile;
168
+ }
169
+
170
+ let sortBy;
171
+ let sortDesc = false;
172
+ function clickHeader(varNm) {
173
+ if (sortBy === varNm) sortDesc = !sortDesc;
174
+ else sortBy = varNm;
175
+ let getter = (x) => x[sortBy];
176
+ if (sortBy === "uploaded_at") getter = (x) => new Date(x[sortBy]);
177
+ if (sortBy === "filename") getter = (x) => (x[sortBy] || "").toLowerCase();
178
+ const cmp = (a, b) => {
179
+ if (getter(a) < getter(b)) return sortDesc ? 1 : -1;
180
+ if (getter(a) > getter(b)) return sortDesc ? -1 : 1;
181
+ return 0;
182
+ };
183
+ files = files.sort(cmp);
184
+ }
185
+ function getSorterIcon(varNm) {
186
+ console.log({ varNm, sortBy });
187
+ if (varNm !== sortBy) return null;
188
+ return sortDesc ? faCaretDown : faCaretUp;
189
+ }
190
+ </script>
191
+
192
+ <main>
193
+ <div class="row">
194
+ <div class="col-8">
195
+ <div>
196
+ <nav aria-label="breadcrumb">
197
+ <ol class="breadcrumb">
198
+ {#each pathSegments as segment}
199
+ <li
200
+ class="breadcrumb-item"
201
+ on:click={gotoFolder(segment.location)}
202
+ >
203
+ {#if segment.icon}
204
+ <Fa icon={segment.icon} />
205
+ {:else}
206
+ {segment.name}
207
+ {/if}
208
+ </li>
209
+ {/each}
210
+ </ol>
211
+ </nav>
212
+ </div>
213
+ <div class="filelist">
214
+ <table class="table table-sm">
215
+ <thead>
216
+ <tr>
217
+ <th />
218
+ <th on:click={() => clickHeader("filename")}>
219
+ Filename
220
+ <Fa icon={getSorterIcon("filename", sortBy, sortDesc)} />
221
+ </th>
222
+ <th on:click={() => clickHeader("mimetype")}>
223
+ Media type
224
+ <Fa icon={getSorterIcon("mimetype", sortBy, sortDesc)} />
225
+ </th>
226
+ <th
227
+ on:click={() => clickHeader("size_kb")}
228
+ style="text-align: right"
229
+ >
230
+ <Fa icon={getSorterIcon("size_kb", sortBy, sortDesc)} />
231
+ Size (KiB)
232
+ </th>
233
+ <th on:click={() => clickHeader("min_role_read")}>
234
+ Role to access
235
+ <Fa icon={getSorterIcon("min_role_read", sortBy, sortDesc)} />
236
+ </th>
237
+ <th on:click={() => clickHeader("uploaded_at")}>
238
+ Created
239
+ <Fa icon={getSorterIcon("uploaded_at", sortBy, sortDesc)} />
240
+ </th>
241
+ </tr>
242
+ </thead>
243
+ <tbody>
244
+ {#each files as file}
245
+ <tr
246
+ on:click={(e) => rowClick(file, e)}
247
+ on:dblclick={() => {
248
+ if (file.isDirectory) gotoFolder(file.location);
249
+ else window.open(`/files/serve/${file.location}`);
250
+ }}
251
+ class:selected={selectedFiles[file.filename]}
252
+ >
253
+ <td>
254
+ <Fa size="lg" icon={getIcon(file)} />
255
+ </td>
256
+ <td>
257
+ {#if file.isDirectory}
258
+ {file.filename}/
259
+ {:else}
260
+ {file.filename}
261
+ {/if}
262
+ </td>
263
+ <td>
264
+ {file.mimetype}
265
+ </td>
266
+ <td style="text-align: right">
267
+ {file.isDirectory ? "" : file.size_kb}
268
+ </td>
269
+ <td>
270
+ {roles[file.min_role_read]}
271
+ </td>
272
+ <td>
273
+ {new Date(file.uploaded_at).toLocaleString()}
274
+ </td>
275
+ </tr>
276
+ {/each}
277
+ <tr on:click={() => window.create_new_folder(currentFolder)}>
278
+ <td>
279
+ <Fa size="lg" icon={faFolderPlus} />
280
+ </td>
281
+ <td>Create new folder...</td>
282
+ <td />
283
+ <td />
284
+ <td />
285
+ </tr>
286
+ </tbody>
287
+ </table>
288
+ </div>
289
+ </div>
290
+
291
+ <div class="col-4">
292
+ {#if selectedList.length > 0}
293
+ <h5>{lastSelected.filename}</h5>
294
+
295
+ {#if lastSelected.mime_super === "image"}
296
+ <img
297
+ class="file-preview my-2"
298
+ src={`/files/serve/${lastSelected.location}`}
299
+ alt={lastSelected.filename}
300
+ />
301
+ {/if}
302
+ <table>
303
+ <tbody>
304
+ {#if !lastSelected.isDirectory}
305
+ <tr>
306
+ <th>Size</th>
307
+ <td>{lastSelected.size_kb} KB</td>
308
+ </tr>
309
+ {/if}
310
+
311
+ <tr>
312
+ <th>MIME type</th>
313
+ <td>
314
+ {#if lastSelected.isDirectory}
315
+ Directory
316
+ {:else}
317
+ {lastSelected.mime_super}/{lastSelected.mime_sub}
318
+ {/if}
319
+ </td>
320
+ </tr>
321
+ <tr>
322
+ <th>Created</th>
323
+ <td>{new Date(lastSelected.uploaded_at).toLocaleString()}</td>
324
+ </tr>
325
+ <tr>
326
+ <th class="pe-1">Role to access</th>
327
+ <td>{roles[lastSelected.min_role_read]}</td>
328
+ </tr>
329
+ </tbody>
330
+ </table>
331
+ <div>
332
+ <a href={`/files/serve/${lastSelected.location}`}>Link</a>
333
+ &nbsp;|&nbsp;
334
+ <a href={`/files/download/${lastSelected.location}`}>Download</a>
335
+ </div>
336
+ {#if selectedList.length > 1}
337
+ <strong
338
+ >and {selectedList.length - 1} other file{selectedList.length > 2
339
+ ? "s"
340
+ : ""}:
341
+ </strong>
342
+ {/if}
343
+ <div class="file-actions d-flex">
344
+ <select class="form-select" on:change={changeAccessRole}>
345
+ <option value="" disabled selected>Set access</option>
346
+ {#each rolesList as role}
347
+ <option value={role.id}>{role.role}</option>
348
+ {/each}
349
+ </select>
350
+
351
+ <select class="form-select" on:change={moveDirectory}>
352
+ <option value="" disabled selected>Move to...</option>
353
+ {#each directories as dir}
354
+ <option>{dir.location || "/"}</option>
355
+ {/each}
356
+ </select>
357
+ <select class="form-select" on:change={goAction}>
358
+ <option value="" disabled selected>Action...</option>
359
+ <option>Delete</option>
360
+ {#if selectedList.length === 1}
361
+ <option>Rename</option>
362
+ {/if}
363
+ </select>
364
+ </div>
365
+ {/if}
366
+ </div>
367
+ </div>
368
+ </main>
369
+
370
+ <style>
371
+ tr.selected {
372
+ background-color: rgb(213, 237, 255);
373
+ }
374
+ img.file-preview {
375
+ max-height: 200px;
376
+ max-width: 100%;
377
+ }
378
+ div.file-actions select {
379
+ width: unset;
380
+ display: inline;
381
+ max-width: 33%;
382
+ }
383
+ div.filelist {
384
+ max-height: 90vh;
385
+ overflow-y: scroll;
386
+ }
387
+ </style>
package/src/main.js ADDED
@@ -0,0 +1,10 @@
1
+ import App from './App.svelte';
2
+
3
+ const app = new App({
4
+ target: document.getElementById("saltcorn-file-manager"),
5
+ props: {
6
+ name: 'world'
7
+ }
8
+ });
9
+
10
+ export default app;