lfss 0.1.0__tar.gz → 0.2.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.1.0 → lfss-0.2.1}/PKG-INFO +13 -8
- lfss-0.2.1/Readme.md +28 -0
- lfss-0.2.1/docs/Permission.md +29 -0
- lfss-0.2.1/frontend/api.js +178 -0
- lfss-0.2.1/frontend/index.html +60 -0
- lfss-0.2.1/frontend/scripts.js +419 -0
- lfss-0.2.1/frontend/styles.css +211 -0
- lfss-0.2.1/frontend/utils.js +83 -0
- lfss-0.2.1/lfss/cli/panel.py +45 -0
- {lfss-0.1.0 → lfss-0.2.1}/lfss/cli/user.py +16 -3
- {lfss-0.1.0 → lfss-0.2.1}/lfss/src/database.py +147 -38
- lfss-0.2.1/lfss/src/error.py +6 -0
- {lfss-0.1.0 → lfss-0.2.1}/lfss/src/server.py +99 -28
- {lfss-0.1.0 → lfss-0.2.1}/pyproject.toml +3 -1
- lfss-0.1.0/Readme.md +0 -23
- {lfss-0.1.0 → lfss-0.2.1}/lfss/cli/serve.py +0 -0
- {lfss-0.1.0 → lfss-0.2.1}/lfss/src/__init__.py +0 -0
- {lfss-0.1.0 → lfss-0.2.1}/lfss/src/config.py +0 -0
- {lfss-0.1.0 → lfss-0.2.1}/lfss/src/log.py +0 -0
- {lfss-0.1.0 → lfss-0.2.1}/lfss/src/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: li, mengxun
|
@@ -18,6 +18,7 @@ Project-URL: Repository, https://github.com/MenxLi/lfss
|
|
18
18
|
Description-Content-Type: text/markdown
|
19
19
|
|
20
20
|
# Lightweight File Storage Service (LFSS)
|
21
|
+
[](https://pypi.org/project/lfss/)
|
21
22
|
|
22
23
|
A lightweight file/object storage service!
|
23
24
|
|
@@ -28,15 +29,19 @@ lfss-user add <username> <password>
|
|
28
29
|
lfss-serve
|
29
30
|
```
|
30
31
|
|
31
|
-
By default, the data will be stored in
|
32
|
-
|
32
|
+
By default, the data will be stored in `.storage_data`.
|
33
|
+
You can change storage directory using the `LFSS_DATA` environment variable.
|
33
34
|
|
34
35
|
I provide a simple client to interact with the service.
|
35
|
-
Just start a web server at `/frontend` and open `index.html` in your browser
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
Just start a web server at `/frontend` and open `index.html` in your browser, or use:
|
37
|
+
```sh
|
38
|
+
lfss-panel
|
39
|
+
```
|
39
40
|
|
40
41
|
The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
|
41
42
|
Authentication is done via `Authorization` header, with the value `Bearer <token>`.
|
42
|
-
|
43
|
+
You can refer to `frontend` as an application example, and `frontend/api.js` for the API usage.
|
44
|
+
|
45
|
+
By default, the service exposes all files to the public for `GET` requests,
|
46
|
+
but file-listing is restricted to the user's own files.
|
47
|
+
Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
|
lfss-0.2.1/Readme.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Lightweight File Storage Service (LFSS)
|
2
|
+
[](https://pypi.org/project/lfss/)
|
3
|
+
|
4
|
+
A lightweight file/object storage service!
|
5
|
+
|
6
|
+
Usage:
|
7
|
+
```sh
|
8
|
+
pip install .
|
9
|
+
lfss-user add <username> <password>
|
10
|
+
lfss-serve
|
11
|
+
```
|
12
|
+
|
13
|
+
By default, the data will be stored in `.storage_data`.
|
14
|
+
You can change storage directory using the `LFSS_DATA` environment variable.
|
15
|
+
|
16
|
+
I provide a simple client to interact with the service.
|
17
|
+
Just start a web server at `/frontend` and open `index.html` in your browser, or use:
|
18
|
+
```sh
|
19
|
+
lfss-panel
|
20
|
+
```
|
21
|
+
|
22
|
+
The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
|
23
|
+
Authentication is done via `Authorization` header, with the value `Bearer <token>`.
|
24
|
+
You can refer to `frontend` as an application example, and `frontend/api.js` for the API usage.
|
25
|
+
|
26
|
+
By default, the service exposes all files to the public for `GET` requests,
|
27
|
+
but file-listing is restricted to the user's own files.
|
28
|
+
Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
# Permission System
|
3
|
+
There are two roles in the system: Admin and User (you can treat then as buckets).
|
4
|
+
|
5
|
+
## `PUT` and `DELETE` permissions
|
6
|
+
Non-login user don't have `PUT/DELETE` permissions.
|
7
|
+
Every user can have `PUT/DELETE` permissions of files under its own `/<user>/` path.
|
8
|
+
The admin can have `PUT/DELETE` permissions of files of all users.
|
9
|
+
|
10
|
+
## `GET` permissions
|
11
|
+
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 `/`).
|
12
|
+
|
13
|
+
### Path-listing
|
14
|
+
- Non-login users cannot list any files.
|
15
|
+
- All users can list the files under their own path
|
16
|
+
- Admins can list the files under other users' path.
|
17
|
+
|
18
|
+
### File-access
|
19
|
+
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.
|
20
|
+
(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.)
|
21
|
+
|
22
|
+
There are four types of permissions: `unset`, `public`, `protected`, `private`.
|
23
|
+
Non-admin users can access files based on:
|
24
|
+
|
25
|
+
- If the file is `public`, then all users can access it.
|
26
|
+
- If the file is `protected`, then only the logged-in user can access it.
|
27
|
+
- If the file is `private`, then only the owner can access it.
|
28
|
+
- If the file is `unset`, then the file's permission is inherited from the owner's permission.
|
29
|
+
- If both the owner and the file have `unset` permission, then the file is `public`.
|
@@ -0,0 +1,178 @@
|
|
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
|
+
*
|
24
|
+
* Partially complete...
|
25
|
+
* @typedef {Object} DirectoryRecord
|
26
|
+
* @property {string} url - the url of the directory
|
27
|
+
* @property {string} size - the size of the directory, in bytes
|
28
|
+
*
|
29
|
+
* @typedef {Object} PathListResponse
|
30
|
+
* @property {DirectoryRecord[]} dirs - the list of directories in the directory
|
31
|
+
* @property {FileRecord[]} files - the list of files in the directory
|
32
|
+
*
|
33
|
+
*/
|
34
|
+
|
35
|
+
export const permMap = {
|
36
|
+
0: 'unset',
|
37
|
+
1: 'public',
|
38
|
+
2: 'protected',
|
39
|
+
3: 'private'
|
40
|
+
}
|
41
|
+
|
42
|
+
export default class Connector {
|
43
|
+
|
44
|
+
constructor(){
|
45
|
+
/** @type {Config} */
|
46
|
+
this.config = {
|
47
|
+
endpoint: 'http://localhost:8000',
|
48
|
+
token: ''
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* @param {string} path - the path to the file (url)
|
54
|
+
* @param {File} file - the file to upload
|
55
|
+
* @returns {Promise<string>} - the promise of the request, the url of the file
|
56
|
+
*/
|
57
|
+
async put(path, file){
|
58
|
+
if (path.startsWith('/')){ path = path.slice(1); }
|
59
|
+
const fileBytes = await file.arrayBuffer();
|
60
|
+
const res = await fetch(this.config.endpoint + '/' + path, {
|
61
|
+
method: 'PUT',
|
62
|
+
headers: {
|
63
|
+
'Authorization': 'Bearer ' + this.config.token,
|
64
|
+
'Content-Type': 'application/octet-stream'
|
65
|
+
},
|
66
|
+
body: fileBytes
|
67
|
+
});
|
68
|
+
if (res.status != 200 && res.status != 201){
|
69
|
+
throw new Error(`Failed to upload file, status code: ${res.status}, message: ${await res.json()}`);
|
70
|
+
}
|
71
|
+
return (await res.json()).url;
|
72
|
+
}
|
73
|
+
|
74
|
+
/**
|
75
|
+
* @param {string} path - the path to the file (url), should end with .json
|
76
|
+
* @param {Objec} data - the data to upload
|
77
|
+
* @returns {Promise<string>} - the promise of the request, the url of the file
|
78
|
+
*/
|
79
|
+
async putJson(path, data){
|
80
|
+
if (!path.endsWith('.json')){ throw new Error('Upload object must end with .json'); }
|
81
|
+
if (path.startsWith('/')){ path = path.slice(1); }
|
82
|
+
const res = await fetch(this.config.endpoint + '/' + path, {
|
83
|
+
method: 'PUT',
|
84
|
+
headers: {
|
85
|
+
'Authorization': 'Bearer ' + this.config.token,
|
86
|
+
'Content-Type': 'application/json'
|
87
|
+
},
|
88
|
+
body: JSON.stringify(data)
|
89
|
+
});
|
90
|
+
if (res.status != 200 && res.status != 201){
|
91
|
+
throw new Error(`Failed to upload object, status code: ${res.status}, message: ${await res.json()}`);
|
92
|
+
}
|
93
|
+
return (await res.json()).url;
|
94
|
+
}
|
95
|
+
|
96
|
+
async delete(path){
|
97
|
+
if (path.startsWith('/')){ path = path.slice(1); }
|
98
|
+
const res = await fetch(this.config.endpoint + '/' + path, {
|
99
|
+
method: 'DELETE',
|
100
|
+
headers: {
|
101
|
+
'Authorization': 'Bearer ' + this.config.token
|
102
|
+
},
|
103
|
+
});
|
104
|
+
if (res.status == 200) return;
|
105
|
+
throw new Error(`Failed to delete file, status code: ${res.status}, message: ${await res.json()}`);
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* @param {string} path - the path to the file
|
110
|
+
* @returns {Promise<FileRecord | null>} - the promise of the request
|
111
|
+
*/
|
112
|
+
async getMetadata(path){
|
113
|
+
if (path.startsWith('/')){ path = path.slice(1); }
|
114
|
+
const res = await fetch(this.config.endpoint + '/_api/fmeta?path=' + path, {
|
115
|
+
method: 'GET',
|
116
|
+
headers: {
|
117
|
+
'Authorization': 'Bearer ' + this.config.token
|
118
|
+
},
|
119
|
+
});
|
120
|
+
if (res.status == 404){
|
121
|
+
return null;
|
122
|
+
}
|
123
|
+
return await res.json();
|
124
|
+
}
|
125
|
+
|
126
|
+
/**
|
127
|
+
* @param {string} path - the path to the file directory, should ends with '/'
|
128
|
+
* @returns {Promise<PathListResponse>} - the promise of the request
|
129
|
+
*/
|
130
|
+
async listPath(path){
|
131
|
+
if (path.startsWith('/')){ path = path.slice(1); }
|
132
|
+
if (!path.endsWith('/')){ path += '/'; }
|
133
|
+
const res = await fetch(this.config.endpoint + '/' + path, {
|
134
|
+
method: 'GET',
|
135
|
+
headers: {
|
136
|
+
'Authorization': 'Bearer ' + this.config.token
|
137
|
+
},
|
138
|
+
});
|
139
|
+
if (res.status == 403 || res.status == 401){
|
140
|
+
throw new Error(`Access denied to ${path}`);
|
141
|
+
}
|
142
|
+
return await res.json();
|
143
|
+
}
|
144
|
+
|
145
|
+
/**
|
146
|
+
* Check the user information by the token
|
147
|
+
* @returns {Promise<UserRecord>} - the promise of the request
|
148
|
+
*/
|
149
|
+
async whoami(){
|
150
|
+
const res = await fetch(this.config.endpoint + '/_api/whoami', {
|
151
|
+
method: 'GET',
|
152
|
+
headers: {
|
153
|
+
'Authorization': 'Bearer ' + this.config.token
|
154
|
+
},
|
155
|
+
});
|
156
|
+
if (res.status != 200){
|
157
|
+
throw new Error('Failed to get user info, status code: ' + res.status);
|
158
|
+
}
|
159
|
+
return await res.json();
|
160
|
+
};
|
161
|
+
|
162
|
+
/**
|
163
|
+
* @param {string} path - file path(url)
|
164
|
+
* @param {number} permission - please refer to the permMap
|
165
|
+
*/
|
166
|
+
async setFilePermission(path, permission){
|
167
|
+
if (path.startsWith('/')){ path = path.slice(1); }
|
168
|
+
const res = await fetch(this.config.endpoint + '/_api/fmeta?path=' + path + '&perm=' + permission, {
|
169
|
+
method: 'POST',
|
170
|
+
headers: {
|
171
|
+
'Authorization': 'Bearer ' + this.config.token
|
172
|
+
},
|
173
|
+
});
|
174
|
+
if (res.status != 200){
|
175
|
+
throw new Error(`Failed to set permission, status code: ${res.status}, message: ${await res.json()}`);
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|
@@ -0,0 +1,60 @@
|
|
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" id="settings">
|
12
|
+
<div class="input-group" style="min-width: 300px;">
|
13
|
+
<label for="endpoint">Endpoint</label>
|
14
|
+
<input type="text" id="endpoint" placeholder="http://localhost:8000" autocomplete="off">
|
15
|
+
</div>
|
16
|
+
<div class="input-group" style="min-width: 800px;">
|
17
|
+
<label for="token">Token</label>
|
18
|
+
<input type="text" id="token" placeholder="" autocomplete="off">
|
19
|
+
</div>
|
20
|
+
<div class="input-group">
|
21
|
+
<span id='back-btn'>⬅️</span>
|
22
|
+
<input type="text" id="path" placeholder="path/to/files/" autocomplete="off">
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
|
26
|
+
<div class="container content" id="view">
|
27
|
+
<div id="position-hint" class='disconnected'>
|
28
|
+
<span></span>
|
29
|
+
<label></label>
|
30
|
+
</div>
|
31
|
+
<table id="files">
|
32
|
+
<thead>
|
33
|
+
<tr>
|
34
|
+
<th>File</th>
|
35
|
+
<th>Size</th>
|
36
|
+
<th>Accessed</th>
|
37
|
+
<th>Created</th>
|
38
|
+
<th>Read access</th>
|
39
|
+
<th>Actions</th>
|
40
|
+
</tr>
|
41
|
+
</thead>
|
42
|
+
<tbody id="files-table-body">
|
43
|
+
</tbody>
|
44
|
+
</table>
|
45
|
+
</div>
|
46
|
+
|
47
|
+
<div class="container footer" id="upload">
|
48
|
+
<div class="input-group">
|
49
|
+
<input type="file" id="file-selector" accept="*">
|
50
|
+
<label for="file-name" id="upload-file-prefix"></label>
|
51
|
+
<input type="text" id="file-name" placeholder="Destination file path" autocomplete="off">
|
52
|
+
<span id='randomize-fname-btn'>🎲</span>
|
53
|
+
<button id="upload-btn">Upload</button>
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
|
57
|
+
</body>
|
58
|
+
|
59
|
+
<script src="./scripts.js" type="module" async defer></script>
|
60
|
+
</html>
|