lfss 0.7.15__tar.gz → 0.8.1__tar.gz

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.
Files changed (44) hide show
  1. {lfss-0.7.15 → lfss-0.8.1}/PKG-INFO +5 -3
  2. {lfss-0.7.15 → lfss-0.8.1}/Readme.md +2 -2
  3. {lfss-0.7.15 → lfss-0.8.1}/docs/Permission.md +4 -2
  4. lfss-0.8.1/frontend/api.js +482 -0
  5. lfss-0.8.1/frontend/index.html +91 -0
  6. lfss-0.8.1/frontend/login.css +21 -0
  7. lfss-0.8.1/frontend/login.js +83 -0
  8. {lfss-0.7.15 → lfss-0.8.1}/frontend/scripts.js +77 -88
  9. {lfss-0.7.15 → lfss-0.8.1}/frontend/state.js +19 -4
  10. {lfss-0.7.15 → lfss-0.8.1}/frontend/styles.css +26 -8
  11. {lfss-0.7.15 → lfss-0.8.1}/frontend/thumb.css +6 -0
  12. {lfss-0.7.15 → lfss-0.8.1}/frontend/thumb.js +6 -2
  13. {lfss-0.7.15/lfss/client → lfss-0.8.1/lfss/api}/__init__.py +72 -41
  14. lfss-0.8.1/lfss/api/connector.py +261 -0
  15. {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/cli.py +1 -1
  16. {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/user.py +1 -1
  17. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/config.py +1 -1
  18. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/connection_pool.py +3 -2
  19. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/database.py +193 -100
  20. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/datatype.py +8 -3
  21. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/error.py +3 -1
  22. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/server.py +147 -61
  23. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/stat.py +1 -1
  24. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/utils.py +47 -13
  25. {lfss-0.7.15 → lfss-0.8.1}/pyproject.toml +3 -1
  26. lfss-0.7.15/frontend/api.js +0 -219
  27. lfss-0.7.15/frontend/index.html +0 -79
  28. lfss-0.7.15/lfss/client/api.py +0 -143
  29. {lfss-0.7.15 → lfss-0.8.1}/docs/Known_issues.md +0 -0
  30. {lfss-0.7.15 → lfss-0.8.1}/frontend/info.css +0 -0
  31. {lfss-0.7.15 → lfss-0.8.1}/frontend/info.js +0 -0
  32. {lfss-0.7.15 → lfss-0.8.1}/frontend/popup.css +0 -0
  33. {lfss-0.7.15 → lfss-0.8.1}/frontend/popup.js +0 -0
  34. {lfss-0.7.15 → lfss-0.8.1}/frontend/utils.js +0 -0
  35. {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/balance.py +0 -0
  36. {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/panel.py +0 -0
  37. {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/serve.py +0 -0
  38. {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/vacuum.py +0 -0
  39. {lfss-0.7.15 → lfss-0.8.1}/lfss/sql/init.sql +0 -0
  40. {lfss-0.7.15 → lfss-0.8.1}/lfss/sql/pragma.sql +0 -0
  41. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/__init__.py +0 -0
  42. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/bounded_pool.py +0 -0
  43. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/log.py +0 -0
  44. {lfss-0.7.15 → lfss-0.8.1}/lfss/src/thumb.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.15
3
+ Version: 0.8.1
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -16,6 +16,8 @@ Requires-Dist: aiosqlite (==0.*)
16
16
  Requires-Dist: fastapi (==0.*)
17
17
  Requires-Dist: mimesniff (==1.*)
18
18
  Requires-Dist: pillow
19
+ Requires-Dist: python-multipart
20
+ Requires-Dist: requests (==2.*)
19
21
  Requires-Dist: uvicorn (==0.*)
20
22
  Project-URL: Repository, https://github.com/MenxLi/lfss
21
23
  Description-Content-Type: text/markdown
@@ -23,7 +25,7 @@ Description-Content-Type: text/markdown
23
25
  # Lightweight File Storage Service (LFSS)
24
26
  [![PyPI](https://img.shields.io/pypi/v/lfss)](https://pypi.org/project/lfss/)
25
27
 
26
- My experiment on a lightweight file/object storage service.
28
+ My experiment on a lightweight and high-performance file/object storage service.
27
29
  It stores small files and metadata in sqlite, large files in the filesystem.
28
30
  Tested on 2 million files, and it works fine...
29
31
 
@@ -45,7 +47,7 @@ Or, you can start a web server at `/frontend` and open `index.html` in your brow
45
47
 
46
48
  The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
47
49
  Authentication is done via `Authorization` header with the value `Bearer <token>`, or through the `token` query parameter.
48
- You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss/client/api.py` for the API usage.
50
+ You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss/api/connector.py` for the API usage.
49
51
 
50
52
  By default, the service exposes all files to the public for `GET` requests,
51
53
  but file-listing is restricted to the user's own files.
@@ -1,7 +1,7 @@
1
1
  # Lightweight File Storage Service (LFSS)
2
2
  [![PyPI](https://img.shields.io/pypi/v/lfss)](https://pypi.org/project/lfss/)
3
3
 
4
- My experiment on a lightweight file/object storage service.
4
+ My experiment on a lightweight and high-performance file/object storage service.
5
5
  It stores small files and metadata in sqlite, large files in the filesystem.
6
6
  Tested on 2 million files, and it works fine...
7
7
 
@@ -23,7 +23,7 @@ Or, you can start a web server at `/frontend` and open `index.html` in your brow
23
23
 
24
24
  The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
25
25
  Authentication is done via `Authorization` header with the value `Bearer <token>`, or through the `token` query parameter.
26
- You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss/client/api.py` for the API usage.
26
+ You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss/api/connector.py` for the API usage.
27
27
 
28
28
  By default, the service exposes all files to the public for `GET` requests,
29
29
  but file-listing is restricted to the user's own files.
@@ -1,13 +1,15 @@
1
1
 
2
2
  # Permission System
3
- There are two roles in the system: Admin and User ('user' are like 'bucket' to some extent).
3
+ There are two user roles in the system: Admin and Normal User ("users" are like "buckets" to some extent).
4
+
5
+ ## Ownership
6
+ A file is owned by the user who created it, may not necessarily be the user under whose path the file is stored (admin can create files under any user's path).
4
7
 
5
8
  ## File access with `GET` permission
6
9
  The `GET` is used to access the file (if path is not ending with `/`), or to list the files under a path (if path is ending with `/`).
7
10
 
8
11
  ### File access
9
12
  For accessing file content, the user must have `GET` permission of the file, which is determined by the `permission` field of both the owner and the file.
10
- (Note: The owner of the file is the user who created the file, may not necessarily be the user under whose path the file is stored.)
11
13
 
12
14
  There are four types of permissions: `unset`, `public`, `protected`, `private`.
13
15
  Non-admin users can access files based on:
@@ -0,0 +1,482 @@
1
+
2
+ /**
3
+ * @typedef Config
4
+ * @property {string} endpoint - the endpoint of the API
5
+ * @property {string} token - the token to authenticate the user
6
+ *
7
+ * Partially complete...
8
+ * @typedef {Object} UserRecord
9
+ * @property {number} id - the id of the user
10
+ * @property {string} username - the username of the user
11
+ * @property {boolean} is_admin - whether the user is an admin
12
+ * @property {string} create_time - the time the user was created
13
+ * @property {number} max_storage - the maximum storage the user can use
14
+ * @property {number} permission - the default read permission of the files owned by the user
15
+ *
16
+ * Partially complete...
17
+ * @typedef {Object} FileRecord
18
+ * @property {string} url - the url of the file
19
+ * @property {number} owner_id - the id of the owner of the file
20
+ * @property {number} file_size - the size of the file, in bytes
21
+ * @property {string} create_time - the time the file was created
22
+ * @property {string} access_time - the time the file was last accessed
23
+ * @property {string} mime_type - the mime type of the file
24
+ *
25
+ * Partially complete...
26
+ * @typedef {Object} DirectoryRecord
27
+ * @property {string} url - the url of the directory
28
+ * @property {string} size - the size of the directory, in bytes
29
+ * @property {string} create_time - the time the directory was created
30
+ * @property {string} access_time - the time the directory was last accessed
31
+ * @property {number} n_files - the number of total files in the directory, including subdirectories
32
+ *
33
+ * @typedef {Object} PathListResponse
34
+ * @property {DirectoryRecord[]} dirs - the list of directories in the directory
35
+ * @property {FileRecord[]} files - the list of files in the directory
36
+ *
37
+ * @typedef {"" | "url" | "file_size" | "create_time" | "access_time" | "mime_type"} FileSortKey
38
+ * @typedef {"" | "dirname" } DirectorySortKey
39
+ */
40
+
41
+ export const permMap = {
42
+ 0: 'unset',
43
+ 1: 'public',
44
+ 2: 'protected',
45
+ 3: 'private'
46
+ }
47
+
48
+ export default class Connector {
49
+
50
+ constructor(){
51
+ /** @type {Config} */
52
+ this.config = {
53
+ endpoint: 'http://localhost:8000',
54
+ token: ''
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @param {string} path - the path to the file (url)
60
+ * @param {File} file - the file to upload
61
+ * @returns {Promise<string>} - the promise of the request, the url of the file
62
+ */
63
+ async put(path, file, {
64
+ conflict = 'abort',
65
+ permission = 0
66
+ } = {}){
67
+ if (path.startsWith('/')){ path = path.slice(1); }
68
+ const fileBytes = await file.arrayBuffer();
69
+ const dst = new URL(this.config.endpoint + '/' + path);
70
+ dst.searchParams.append('conflict', conflict);
71
+ dst.searchParams.append('permission', permission);
72
+ const res = await fetch(dst.toString(), {
73
+ method: 'PUT',
74
+ headers: {
75
+ 'Authorization': 'Bearer ' + this.config.token,
76
+ 'Content-Type': 'application/octet-stream',
77
+ 'Content-Length': fileBytes.byteLength
78
+ },
79
+ body: fileBytes
80
+ });
81
+ if (res.status != 200 && res.status != 201){
82
+ throw new Error(`Failed to upload file, status code: ${res.status}, message: ${await res.json()}`);
83
+ }
84
+ return (await res.json()).url;
85
+ }
86
+
87
+ /**
88
+ * @param {string} path - the path to the file (url)
89
+ * @param {File} file - the file to upload
90
+ * @returns {Promise<string>} - the promise of the request, the url of the file
91
+ */
92
+ async post(path, file, {
93
+ conflict = 'abort',
94
+ permission = 0
95
+ } = {}){
96
+ if (path.startsWith('/')){ path = path.slice(1); }
97
+ const dst = new URL(this.config.endpoint + '/' + path);
98
+ dst.searchParams.append('conflict', conflict);
99
+ dst.searchParams.append('permission', permission);
100
+ // post as multipart form data
101
+ const formData = new FormData();
102
+ formData.append('file', file);
103
+ const res = await fetch(dst.toString(), {
104
+ method: 'POST',
105
+ // don't include the content type, let the browser handle it
106
+ // https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/
107
+ headers: {
108
+ 'Authorization': 'Bearer ' + this.config.token,
109
+ },
110
+ body: formData
111
+ });
112
+
113
+ if (res.status != 200 && res.status != 201){
114
+ throw new Error(`Failed to upload file, status code: ${res.status}, message: ${await res.json()}`);
115
+ }
116
+ return (await res.json()).url;
117
+ }
118
+
119
+ /**
120
+ * @param {string} path - the path to the file (url), should end with .json
121
+ * @param {Objec} data - the data to upload
122
+ * @returns {Promise<string>} - the promise of the request, the url of the file
123
+ */
124
+ async putJson(path, data){
125
+ if (!path.endsWith('.json')){ throw new Error('Upload object must end with .json'); }
126
+ if (path.startsWith('/')){ path = path.slice(1); }
127
+ const res = await fetch(this.config.endpoint + '/' + path, {
128
+ method: 'PUT',
129
+ headers: {
130
+ 'Authorization': 'Bearer ' + this.config.token,
131
+ 'Content-Type': 'application/json'
132
+ },
133
+ body: JSON.stringify(data)
134
+ });
135
+ if (res.status != 200 && res.status != 201){
136
+ throw new Error(`Failed to upload object, status code: ${res.status}, message: ${await res.json()}`);
137
+ }
138
+ return (await res.json()).url;
139
+ }
140
+
141
+ async delete(path){
142
+ if (path.startsWith('/')){ path = path.slice(1); }
143
+ const res = await fetch(this.config.endpoint + '/' + path, {
144
+ method: 'DELETE',
145
+ headers: {
146
+ 'Authorization': 'Bearer ' + this.config.token
147
+ },
148
+ });
149
+ if (res.status == 200) return;
150
+ throw new Error(`Failed to delete file, status code: ${res.status}, message: ${await res.json()}`);
151
+ }
152
+
153
+ /**
154
+ * @param {string} path - the path to the file or directory
155
+ * @returns {Promise<FileRecord | DirectoryRecord | null>} - the promise of the request
156
+ */
157
+ async getMetadata(path){
158
+ if (path.startsWith('/')){ path = path.slice(1); }
159
+ const res = await fetch(this.config.endpoint + '/_api/meta?path=' + path, {
160
+ method: 'GET',
161
+ headers: {
162
+ 'Authorization': 'Bearer ' + this.config.token
163
+ },
164
+ });
165
+ if (res.status == 404){
166
+ return null;
167
+ }
168
+ return await res.json();
169
+ }
170
+
171
+ _sanitizeDirPath(path){
172
+ if (path.startsWith('/')){ path = path.slice(1); }
173
+ if (!path.endsWith('/')){ path += '/'; }
174
+ return path;
175
+ }
176
+ /**
177
+ * @param {string} path - the path to the file directory, should ends with '/'
178
+ * @returns {Promise<PathListResponse>} - the promise of the request
179
+ */
180
+ async listPath(path){
181
+ path = this._sanitizeDirPath(path);
182
+ const dst = new URL(this.config.endpoint + '/' + path);
183
+ const res = await fetch(dst.toString(), {
184
+ method: 'GET',
185
+ headers: {
186
+ 'Authorization': 'Bearer ' + this.config.token
187
+ },
188
+ });
189
+ if (res.status == 403 || res.status == 401){
190
+ throw new Error(`Access denied to ${path}`);
191
+ }
192
+ return await res.json();
193
+ }
194
+
195
+ /**
196
+ * @param {string} path - the path to the file directory, should ends with '/'
197
+ * @param {boolean} flat - whether to list the files in subdirectories
198
+ * @returns {Promise<number>} - the promise of the request
199
+ * */
200
+ async countFiles(path, {
201
+ flat = false
202
+ } = {}){
203
+ path = this._sanitizeDirPath(path);
204
+ const dst = new URL(this.config.endpoint + '/_api/count-files');
205
+ dst.searchParams.append('path', path);
206
+ dst.searchParams.append('flat', flat);
207
+ const res = await fetch(dst.toString(), {
208
+ method: 'GET',
209
+ headers: {
210
+ 'Authorization': 'Bearer ' + this.config.token
211
+ },
212
+ });
213
+ if (res.status != 200){
214
+ throw new Error(`Failed to count files, status code: ${res.status}, message: ${await res.json()}`);
215
+ }
216
+ return (await res.json()).count;
217
+ }
218
+
219
+ /**
220
+ * @typedef {Object} ListFilesOptions
221
+ * @property {number} offset - the offset of the list
222
+ * @property {number} limit - the limit of the list
223
+ * @property {FileSortKey} orderBy - the key to order the files
224
+ * @property {boolean} orderDesc - whether to order the files in descending order
225
+ * @property {boolean} flat - whether to list the files in subdirectories
226
+ *
227
+ * @param {string} path - the path to the file directory, should ends with '/'
228
+ * @param {ListFilesOptions} options - the options for the request
229
+ * @returns {Promise<FileRecord[]>} - the promise of the request
230
+ */
231
+ async listFiles(path, {
232
+ offset = 0,
233
+ limit = 1000,
234
+ orderBy = 'create_time',
235
+ orderDesc = false,
236
+ flat = false
237
+ } = {}){
238
+ path = this._sanitizeDirPath(path);
239
+ const dst = new URL(this.config.endpoint + '/_api/list-files');
240
+ dst.searchParams.append('path', path);
241
+ dst.searchParams.append('offset', offset);
242
+ dst.searchParams.append('limit', limit);
243
+ dst.searchParams.append('order_by', orderBy);
244
+ dst.searchParams.append('order_desc', orderDesc);
245
+ dst.searchParams.append('flat', flat);
246
+ const res = await fetch(dst.toString(), {
247
+ method: 'GET',
248
+ headers: {
249
+ 'Authorization': 'Bearer ' + this.config.token
250
+ },
251
+ });
252
+ if (res.status != 200){
253
+ throw new Error(`Failed to list files, status code: ${res.status}, message: ${await res.json()}`);
254
+ }
255
+ return await res.json();
256
+ }
257
+
258
+ /**
259
+ * @param {string} path - the path to the file directory, should ends with '/'
260
+ * @returns {Promise<number>} - the promise of the request
261
+ **/
262
+ async countDirs(path){
263
+ path = this._sanitizeDirPath(path);
264
+ const dst = new URL(this.config.endpoint + '/_api/count-dirs');
265
+ dst.searchParams.append('path', path);
266
+ const res = await fetch(dst.toString(), {
267
+ method: 'GET',
268
+ headers: {
269
+ 'Authorization': 'Bearer ' + this.config.token
270
+ },
271
+ });
272
+ if (res.status != 200){
273
+ throw new Error(`Failed to count directories, status code: ${res.status}, message: ${await res.json()}`);
274
+ }
275
+ return (await res.json()).count;
276
+ }
277
+
278
+ /**
279
+ * @typedef {Object} ListDirsOptions
280
+ * @property {number} offset - the offset of the list
281
+ * @property {number} limit - the limit of the list
282
+ * @property {DirectorySortKey} orderBy - the key to order the directories
283
+ * @property {boolean} orderDesc - whether to order the directories in descending order
284
+ * @property {boolean} skim - whether to skim the directories
285
+ *
286
+ * @param {string} path - the path to the file directory, should ends with '/'
287
+ * @param {ListDirsOptions} options - the options for the request
288
+ * @returns {Promise<DirectoryRecord[]>} - the promise of the request
289
+ **/
290
+ async listDirs(path, {
291
+ offset = 0,
292
+ limit = 1000,
293
+ orderBy = 'dirname',
294
+ orderDesc = false,
295
+ skim = true
296
+ } = {}){
297
+ path = this._sanitizeDirPath(path);
298
+ const dst = new URL(this.config.endpoint + '/_api/list-dirs');
299
+ dst.searchParams.append('path', path);
300
+ dst.searchParams.append('offset', offset);
301
+ dst.searchParams.append('limit', limit);
302
+ dst.searchParams.append('order_by', orderBy);
303
+ dst.searchParams.append('order_desc', orderDesc);
304
+ dst.searchParams.append('skim', skim);
305
+ const res = await fetch(dst.toString(), {
306
+ method: 'GET',
307
+ headers: {
308
+ 'Authorization': 'Bearer ' + this.config.token
309
+ },
310
+ });
311
+ if (res.status != 200){
312
+ throw new Error(`Failed to list directories, status code: ${res.status}, message: ${await res.json()}`);
313
+ }
314
+ return await res.json();
315
+ }
316
+
317
+ /**
318
+ * Check the user information by the token
319
+ * @returns {Promise<UserRecord>} - the promise of the request
320
+ */
321
+ async whoami(){
322
+ const res = await fetch(this.config.endpoint + '/_api/whoami', {
323
+ method: 'GET',
324
+ headers: {
325
+ 'Authorization': 'Bearer ' + this.config.token
326
+ },
327
+ });
328
+ if (res.status != 200){
329
+ throw new Error('Failed to get user info, status code: ' + res.status);
330
+ }
331
+ return await res.json();
332
+ };
333
+
334
+ /**
335
+ * @param {string} path - file path(url)
336
+ * @param {number} permission - please refer to the permMap
337
+ */
338
+ async setFilePermission(path, permission){
339
+ if (path.startsWith('/')){ path = path.slice(1); }
340
+ const dst = new URL(this.config.endpoint + '/_api/meta');
341
+ dst.searchParams.append('path', path);
342
+ dst.searchParams.append('perm', permission);
343
+ const res = await fetch(dst.toString(), {
344
+ method: 'POST',
345
+ headers: {
346
+ 'Authorization': 'Bearer ' + this.config.token
347
+ },
348
+ });
349
+ if (res.status != 200){
350
+ throw new Error(`Failed to set permission, status code: ${res.status}, message: ${await res.json()}`);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * @param {string} path - file path(url)
356
+ * @param {string} newPath - new file path(url)
357
+ */
358
+ async move(path, newPath){
359
+ if (path.startsWith('/')){ path = path.slice(1); }
360
+ if (newPath.startsWith('/')){ newPath = newPath.slice(1); }
361
+ const dst = new URL(this.config.endpoint + '/_api/meta');
362
+ dst.searchParams.append('path', path);
363
+ dst.searchParams.append('new_path', newPath);
364
+ const res = await fetch(dst.toString(), {
365
+ method: 'POST',
366
+ headers: {
367
+ 'Authorization': 'Bearer ' + this.config.token,
368
+ 'Content-Type': 'application/www-form-urlencoded'
369
+ },
370
+ });
371
+ if (res.status != 200){
372
+ throw new Error(`Failed to move file, status code: ${res.status}, message: ${await res.json()}`);
373
+ }
374
+ }
375
+
376
+ }
377
+
378
+ /**
379
+ * a function to wrap the listDirs and listFiles function into one
380
+ * it will return the list of directories and files in the directory,
381
+ * making directories first (if the offset is less than the number of directories),
382
+ * and files after that.
383
+ *
384
+ *
385
+ * @typedef {Object} ListPathOptions
386
+ * @property {number} offset - the offset of the list
387
+ * @property {number} limit - the limit of the list
388
+ * @property {ListFilesOptions} orderBy - the key to order the files (if set to url, will list the directories using dirname)
389
+ * @property {boolean} orderDesc - whether to order the files in descending order
390
+ *
391
+ * @param {Connector} conn - the connector to the API
392
+ * @param {string} path - the path to the file directory
393
+ * @param {Object} options - the options for the request
394
+ * @returns {Promise<[PathListResponse, {dirs: number, files: number}]>} - the promise of the request
395
+ */
396
+ export async function listPath(conn, path, {
397
+ offset = 0,
398
+ limit = 1000,
399
+ orderBy = '',
400
+ orderDesc = false,
401
+ } = {}){
402
+
403
+ if (path === '/' || path === ''){
404
+ // this handles separate case for the root directory... please refer to the backend implementation
405
+ return [await conn.listPath(''), {dirs: 0, files: 0}];
406
+ }
407
+
408
+ orderBy = orderBy == 'none' ? '' : orderBy;
409
+ console.debug('listPath', path, offset, limit, orderBy, orderDesc);
410
+
411
+ const [dirCount, fileCount] = await Promise.all([
412
+ conn.countDirs(path),
413
+ conn.countFiles(path)
414
+ ]);
415
+
416
+ const dirOffset = offset;
417
+ const fileOffset = Math.max(offset - dirCount, 0);
418
+ const dirThispage = Math.max(Math.min(dirCount - dirOffset, limit), 0);
419
+ const fileLimit = limit - dirThispage;
420
+
421
+ console.debug('dirCount', dirCount, 'dirOffset', dirOffset, 'fileOffset', fileOffset, 'dirThispage', dirThispage, 'fileLimit', fileLimit);
422
+
423
+ const dirOrderBy = orderBy == 'url' ? 'dirname' : '';
424
+ const fileOrderBy = orderBy;
425
+
426
+ const [dirList, fileList] = await Promise.all([
427
+ (async () => {
428
+ if (offset < dirCount) {
429
+ return await conn.listDirs(path, {
430
+ offset: dirOffset,
431
+ limit: dirThispage,
432
+ orderBy: dirOrderBy,
433
+ orderDesc: orderDesc
434
+ });
435
+ }
436
+ return [];
437
+ })(),
438
+ (async () => {
439
+ if (fileLimit >= 0 && fileCount > fileOffset) {
440
+ return await conn.listFiles(path, {
441
+ offset: fileOffset,
442
+ limit: fileLimit,
443
+ orderBy: fileOrderBy,
444
+ orderDesc: orderDesc
445
+ });
446
+ }
447
+ return [];
448
+ })()
449
+ ]);
450
+
451
+ return [{
452
+ dirs: dirList,
453
+ files: fileList
454
+ }, {
455
+ dirs: dirCount,
456
+ files: fileCount
457
+ }];
458
+ };
459
+
460
+ /**
461
+ * a function to wrap the upload function into one
462
+ * it will return the url of the file
463
+ *
464
+ * @typedef {Object} UploadOptions
465
+ * @property {string} conflict - the conflict resolution strategy, can be 'abort', 'replace', 'rename'
466
+ * @property {number} permission - the permission of the file, can be 0, 1, 2, 3
467
+ *
468
+ * @param {Connector} conn - the connector to the API
469
+ * @param {string} path - the path to the file (url)
470
+ * @param {File} file - the file to upload
471
+ * @param {UploadOptions} options - the options for the request
472
+ * @returns {Promise<string>} - the promise of the request, the url of the file
473
+ */
474
+ export async function uploadFile(conn, path, file, {
475
+ conflict = 'abort',
476
+ permission = 0
477
+ } = {}){
478
+ if (file.size < 1024 * 1024 * 10){
479
+ return await conn.put(path, file, {conflict, permission});
480
+ }
481
+ return await conn.post(path, file, {conflict, permission});
482
+ }
@@ -0,0 +1,91 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LFSS</title>
7
+ <link rel="stylesheet" href="./styles.css">
8
+ </head>
9
+ <body>
10
+
11
+ <div class="container header">
12
+ <div class="input-group">
13
+ <span id='back-btn'>⬅️</span>
14
+ <input type="text" id="path" placeholder="path/to/files/" autocomplete="off">
15
+ </div>
16
+ <div id="settings-bar">
17
+ <div id="position-hint" class='disconnected'>
18
+ <span></span>
19
+ <label></label>
20
+ </div>
21
+ <div style="color: #999; font-size: small; display: flex; align-items: center; gap: .75rem;">
22
+ <div class="page-config">
23
+ <span>Limit</span>
24
+ <select id="page-limit-sel">
25
+ <option value="10">10</option>
26
+ <option value="50">50</option>
27
+ <option value="100">100</option>
28
+ <option value="500">500</option>
29
+ <option value="1000">1000</option>
30
+ <option value="5000">5000</option>
31
+ </select>
32
+ </div>
33
+
34
+ <div class="page-config">
35
+ <span>Page</span>
36
+ <input type="number" id="page-num-input" value="1" min="1" style="width: 50px;">
37
+ /
38
+ <label id="page-count-lbl">1</label>
39
+ </div>
40
+
41
+ <div class="page-config">
42
+ <span>Sort by</span>
43
+ <select id="sort-by-sel">
44
+ <option value="none">None</option>
45
+ <option value="url">Name</option>
46
+ <option value="file_size">Size</option>
47
+ <option value="access_time">Accessed</option>
48
+ <option value="create_time">Created</option>
49
+ <option value="mime_type">Type</option>
50
+ </select>
51
+ <select id="sort-order-sel">
52
+ <option value="asc">Ascending</option>
53
+ <option value="desc">Descending</option>
54
+ </select>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="container content" id="view">
61
+ <table id="files">
62
+ <thead>
63
+ <tr>
64
+ <th>Name</th>
65
+ <th>Size</th>
66
+ <th>Accessed</th>
67
+ <th>Read access</th>
68
+ <th>Actions</th>
69
+ </tr>
70
+ </thead>
71
+ <tbody id="files-table-body">
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+
76
+ <div class="container footer" id="upload">
77
+ <div class="input-group">
78
+ <input type="file" id="file-selector" accept="*">
79
+ <label for="file-name" id="upload-file-prefix"></label>
80
+ <div class="input-group">
81
+ <input type="text" id="file-name" placeholder="Destination file path" autocomplete="off">
82
+ <span id='randomize-fname-btn'>🎲</span>
83
+ <button id="upload-btn">Upload</button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ </body>
89
+
90
+ <script src="./scripts.js" type="module" async defer></script>
91
+ </html>
@@ -0,0 +1,21 @@
1
+
2
+ div#login-container{
3
+ display: flex;
4
+ flex-direction: column;
5
+ justify-content: center;
6
+ align-items: center;
7
+ gap: 0.5rem;
8
+ width: calc(max(70vw, 420px));
9
+ min-width: 420px;
10
+ }
11
+
12
+ label.login-lbl{
13
+ text-align: left;
14
+ font-weight: bold;
15
+ }
16
+
17
+ button#login-btn{
18
+ padding: 0.8rem;
19
+ padding-inline: 3rem;
20
+ margin-top: 0.5rem;
21
+ }