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.
- {lfss-0.7.15 → lfss-0.8.1}/PKG-INFO +5 -3
- {lfss-0.7.15 → lfss-0.8.1}/Readme.md +2 -2
- {lfss-0.7.15 → lfss-0.8.1}/docs/Permission.md +4 -2
- lfss-0.8.1/frontend/api.js +482 -0
- lfss-0.8.1/frontend/index.html +91 -0
- lfss-0.8.1/frontend/login.css +21 -0
- lfss-0.8.1/frontend/login.js +83 -0
- {lfss-0.7.15 → lfss-0.8.1}/frontend/scripts.js +77 -88
- {lfss-0.7.15 → lfss-0.8.1}/frontend/state.js +19 -4
- {lfss-0.7.15 → lfss-0.8.1}/frontend/styles.css +26 -8
- {lfss-0.7.15 → lfss-0.8.1}/frontend/thumb.css +6 -0
- {lfss-0.7.15 → lfss-0.8.1}/frontend/thumb.js +6 -2
- {lfss-0.7.15/lfss/client → lfss-0.8.1/lfss/api}/__init__.py +72 -41
- lfss-0.8.1/lfss/api/connector.py +261 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/cli.py +1 -1
- {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/user.py +1 -1
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/config.py +1 -1
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/connection_pool.py +3 -2
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/database.py +193 -100
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/datatype.py +8 -3
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/error.py +3 -1
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/server.py +147 -61
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/stat.py +1 -1
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/utils.py +47 -13
- {lfss-0.7.15 → lfss-0.8.1}/pyproject.toml +3 -1
- lfss-0.7.15/frontend/api.js +0 -219
- lfss-0.7.15/frontend/index.html +0 -79
- lfss-0.7.15/lfss/client/api.py +0 -143
- {lfss-0.7.15 → lfss-0.8.1}/docs/Known_issues.md +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/frontend/info.css +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/frontend/info.js +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/frontend/popup.css +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/frontend/popup.js +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/frontend/utils.js +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/balance.py +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/panel.py +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/serve.py +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/cli/vacuum.py +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/sql/init.sql +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/sql/pragma.sql +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/__init__.py +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/bounded_pool.py +0 -0
- {lfss-0.7.15 → lfss-0.8.1}/lfss/src/log.py +0 -0
- {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.
|
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
|
[](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/
|
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
|
[](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/
|
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 (
|
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
|
+
}
|