lfss 0.11.0__py3-none-any.whl → 0.11.1__py3-none-any.whl
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.
- frontend/scripts.js +43 -39
- frontend/utils.js +99 -1
- lfss/api/__init__.py +1 -1
- lfss/api/connector.py +7 -4
- lfss/cli/cli.py +1 -1
- lfss/eng/database.py +8 -8
- {lfss-0.11.0.dist-info → lfss-0.11.1.dist-info}/METADATA +1 -1
- {lfss-0.11.0.dist-info → lfss-0.11.1.dist-info}/RECORD +10 -10
- {lfss-0.11.0.dist-info → lfss-0.11.1.dist-info}/WHEEL +0 -0
- {lfss-0.11.0.dist-info → lfss-0.11.1.dist-info}/entry_points.txt +0 -0
frontend/scripts.js
CHANGED
@@ -5,6 +5,7 @@ import { showInfoPanel, showDirInfoPanel } from './info.js';
|
|
5
5
|
import { makeThumbHtml } from './thumb.js';
|
6
6
|
import { store } from './state.js';
|
7
7
|
import { maybeShowLoginPanel } from './login.js';
|
8
|
+
import { forEachFile } from './utils.js';
|
8
9
|
|
9
10
|
/** @type {import('./api.js').UserRecord}*/
|
10
11
|
let userRecord = null;
|
@@ -158,55 +159,58 @@ uploadFileNameInput.addEventListener('input', debounce(onFileNameInpuChange, 500
|
|
158
159
|
e.preventDefault();
|
159
160
|
e.stopPropagation();
|
160
161
|
});
|
161
|
-
window.addEventListener('drop', (e) => {
|
162
|
+
window.addEventListener('drop', async (e) => {
|
162
163
|
e.preventDefault();
|
163
164
|
e.stopPropagation();
|
164
|
-
const
|
165
|
-
if (
|
166
|
-
uploadFileSelector.files = files;
|
167
|
-
uploadFileNameInput.value = files[0].name;
|
165
|
+
const items = e.dataTransfer.items;
|
166
|
+
if (items.length == 1 && items[0].kind === 'file' && items[0].webkitGetAsEntry().isFile){
|
167
|
+
uploadFileSelector.files = e.dataTransfer.files;
|
168
|
+
uploadFileNameInput.value = e.dataTransfer.files[0].name;
|
168
169
|
uploadFileNameInput.focus();
|
170
|
+
return;
|
169
171
|
}
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
172
|
+
|
173
|
+
/** @type {[string, File][]} */
|
174
|
+
const uploadInputVal = uploadFileNameInput.value? uploadFileNameInput.value : '';
|
175
|
+
let dstPath = store.dirpath + uploadInputVal;
|
176
|
+
if (!dstPath.endsWith('/')){ dstPath += '/'; }
|
177
|
+
|
178
|
+
if (!confirm(`\
|
174
179
|
You are trying to upload multiple files at once.
|
175
180
|
This will directly upload the files to the [${dstPath}] directory without renaming.
|
176
181
|
Note that same name files will be overwritten.
|
177
|
-
Are you sure you want to proceed
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
}
|
186
|
-
catch (err){
|
187
|
-
showPopup('Failed to upload file [' + file.name + ']: ' + err, {level: 'error', timeout: 5000});
|
188
|
-
}
|
189
|
-
counter += 1;
|
190
|
-
console.log("Uploading file: ", counter, "/", files.length);
|
182
|
+
Are you sure you want to proceed?\
|
183
|
+
`)){ return; }
|
184
|
+
|
185
|
+
let counter = 0;
|
186
|
+
async function uploadFileFn(path, file){
|
187
|
+
const this_count = counter;
|
188
|
+
try{
|
189
|
+
await uploadFile(conn, path, file, {conflict: 'overwrite'});
|
191
190
|
}
|
192
|
-
|
193
|
-
|
194
|
-
for (let i = 0; i < files.length; i++){
|
195
|
-
const file = files[i];
|
196
|
-
const path = dstPath + file.name;
|
197
|
-
promises.push(uploadFileFn(file, path));
|
191
|
+
catch (err){
|
192
|
+
showPopup('Failed to upload file [' + file.name + ']: ' + err, {level: 'error', timeout: 5000});
|
198
193
|
}
|
199
|
-
|
200
|
-
Promise.all(promises).then(
|
201
|
-
() => {
|
202
|
-
showPopup('Upload success.', {level: 'success', timeout: 3000});
|
203
|
-
refreshFileList();
|
204
|
-
},
|
205
|
-
(err) => {
|
206
|
-
showPopup('Failed to upload some files: ' + err, {level: 'error', timeout: 5000});
|
207
|
-
}
|
208
|
-
);
|
194
|
+
console.log(`[${this_count}/${counter}] Uploaded file: ${path}`);
|
209
195
|
}
|
196
|
+
|
197
|
+
const promises = await forEachFile(e, async (relPath, filePromise) => {
|
198
|
+
counter += 1;
|
199
|
+
const file = await filePromise;
|
200
|
+
await uploadFileFn(dstPath + relPath, file);
|
201
|
+
});
|
202
|
+
|
203
|
+
showPopup('Uploading multiple files...', {level: 'info', timeout: 3000});
|
204
|
+
Promise.all(promises).then(
|
205
|
+
() => {
|
206
|
+
showPopup('Upload success.', {level: 'success', timeout: 3000});
|
207
|
+
refreshFileList();
|
208
|
+
},
|
209
|
+
(err) => {
|
210
|
+
showPopup('Failed to upload some files: ' + err, {level: 'error', timeout: 5000});
|
211
|
+
}
|
212
|
+
);
|
213
|
+
|
210
214
|
});
|
211
215
|
}
|
212
216
|
|
frontend/utils.js
CHANGED
@@ -93,4 +93,102 @@ export function asHtmlText(text){
|
|
93
93
|
anonElem.textContent = text;
|
94
94
|
const htmlText = anonElem.innerHTML;
|
95
95
|
return htmlText;
|
96
|
-
}
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Iterates over all files dropped in the event,
|
100
|
+
* including files inside directories, and processes them
|
101
|
+
* using the provided callback with a concurrency limit.
|
102
|
+
*
|
103
|
+
* @param {Event} e The drop event.
|
104
|
+
* @param {(relPath: string, file: Promise<File>) => Promise<void>} callback A function
|
105
|
+
* that receives the relative path and a promise for the File.
|
106
|
+
* @param {number} [maxConcurrent=5] Maximum number of concurrent callback executions.
|
107
|
+
* @returns {Promise<Promise<void>[]>} A promise resolving to an array of callback promises.
|
108
|
+
*/
|
109
|
+
export async function forEachFile(e, callback, maxConcurrent = 16) {
|
110
|
+
const results = []; // to collect callback promises
|
111
|
+
|
112
|
+
// Concurrency barrier variables.
|
113
|
+
let activeCount = 0;
|
114
|
+
const queue = [];
|
115
|
+
|
116
|
+
/**
|
117
|
+
* Runs the given async task when below the concurrency limit.
|
118
|
+
* If at limit, waits until a slot is free.
|
119
|
+
*
|
120
|
+
* @param {() => Promise<any>} task An async function returning a promise.
|
121
|
+
* @returns {Promise<any>}
|
122
|
+
*/
|
123
|
+
async function runWithLimit(task) {
|
124
|
+
// If we reached the concurrency limit, wait for a free slot.
|
125
|
+
if (activeCount >= maxConcurrent) {
|
126
|
+
await new Promise(resolve => queue.push(resolve));
|
127
|
+
}
|
128
|
+
activeCount++;
|
129
|
+
try {
|
130
|
+
return await task();
|
131
|
+
} finally {
|
132
|
+
activeCount--;
|
133
|
+
// If there are waiting tasks, allow the next one to run.
|
134
|
+
if (queue.length) {
|
135
|
+
queue.shift()();
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Recursively traverses a file system entry.
|
142
|
+
*
|
143
|
+
* @param {FileSystemEntry} entry The entry (file or directory).
|
144
|
+
* @param {string} path The current relative path.
|
145
|
+
*/
|
146
|
+
async function traverse(entry, path) {
|
147
|
+
if (entry.isFile) {
|
148
|
+
// Wrap file retrieval in a promise.
|
149
|
+
const filePromise = new Promise((resolve, reject) => {
|
150
|
+
entry.file(resolve, reject);
|
151
|
+
});
|
152
|
+
// Use the concurrency barrier for the callback invocation.
|
153
|
+
results.push(runWithLimit(() => callback(path + entry.name, filePromise)));
|
154
|
+
} else if (entry.isDirectory) {
|
155
|
+
const reader = entry.createReader();
|
156
|
+
|
157
|
+
async function readAllEntries(reader) {
|
158
|
+
const entries = [];
|
159
|
+
while (true) {
|
160
|
+
const chunk = await new Promise((resolve, reject) => {
|
161
|
+
reader.readEntries(resolve, reject);
|
162
|
+
});
|
163
|
+
if (chunk.length === 0) break;
|
164
|
+
entries.push(...chunk);
|
165
|
+
}
|
166
|
+
return entries;
|
167
|
+
}
|
168
|
+
|
169
|
+
const entries = await readAllEntries(reader);
|
170
|
+
await Promise.all(
|
171
|
+
entries.map(ent => traverse(ent, path + entry.name + '/'))
|
172
|
+
);
|
173
|
+
}
|
174
|
+
}
|
175
|
+
|
176
|
+
// Process using DataTransfer items if available.
|
177
|
+
if (e.dataTransfer && e.dataTransfer.items) {
|
178
|
+
await Promise.all(
|
179
|
+
Array.from(e.dataTransfer.items).map(async item => {
|
180
|
+
const entry = item.webkitGetAsEntry && item.webkitGetAsEntry();
|
181
|
+
if (entry) {
|
182
|
+
await traverse(entry, '');
|
183
|
+
}
|
184
|
+
})
|
185
|
+
);
|
186
|
+
} else if (e.dataTransfer && e.dataTransfer.files) {
|
187
|
+
// Fallback for browsers that support only dataTransfer.files.
|
188
|
+
Array.from(e.dataTransfer.files).forEach(file => {
|
189
|
+
results.push(runWithLimit(() => callback(file.name, Promise.resolve(file))));
|
190
|
+
});
|
191
|
+
}
|
192
|
+
return results;
|
193
|
+
}
|
194
|
+
|
lfss/api/__init__.py
CHANGED
@@ -113,7 +113,7 @@ def download_file(
|
|
113
113
|
print(f"File {file_path} already exists, skipping download.")
|
114
114
|
return True, error_msg
|
115
115
|
try:
|
116
|
-
fmeta = connector.
|
116
|
+
fmeta = connector.get_meta(src_url)
|
117
117
|
if fmeta is None:
|
118
118
|
error_msg = "File not found."
|
119
119
|
return False, error_msg
|
lfss/api/connector.py
CHANGED
@@ -98,7 +98,7 @@ class Connector:
|
|
98
98
|
|
99
99
|
# Skip ahead by checking if the file already exists
|
100
100
|
if conflict == 'skip-ahead':
|
101
|
-
exists = self.
|
101
|
+
exists = self.get_meta(path)
|
102
102
|
if exists is None:
|
103
103
|
conflict = 'skip'
|
104
104
|
else:
|
@@ -122,7 +122,7 @@ class Connector:
|
|
122
122
|
|
123
123
|
# Skip ahead by checking if the file already exists
|
124
124
|
if conflict == 'skip-ahead':
|
125
|
-
exists = self.
|
125
|
+
exists = self.get_meta(path)
|
126
126
|
if exists is None:
|
127
127
|
conflict = 'skip'
|
128
128
|
else:
|
@@ -154,7 +154,7 @@ class Connector:
|
|
154
154
|
|
155
155
|
# Skip ahead by checking if the file already exists
|
156
156
|
if conflict == 'skip-ahead':
|
157
|
-
exists = self.
|
157
|
+
exists = self.get_meta(path)
|
158
158
|
if exists is None:
|
159
159
|
conflict = 'skip'
|
160
160
|
else:
|
@@ -211,7 +211,7 @@ class Connector:
|
|
211
211
|
"""Deletes the file at the specified path."""
|
212
212
|
self._fetch_factory('DELETE', path)()
|
213
213
|
|
214
|
-
def
|
214
|
+
def get_meta(self, path: str) -> Optional[FileRecord | DirectoryRecord]:
|
215
215
|
"""Gets the metadata for the file at the specified path."""
|
216
216
|
try:
|
217
217
|
response = self._fetch_factory('GET', '_api/meta', {'path': path})()
|
@@ -223,6 +223,9 @@ class Connector:
|
|
223
223
|
if e.response.status_code == 404:
|
224
224
|
return None
|
225
225
|
raise e
|
226
|
+
# shorthand methods for type constraints
|
227
|
+
def get_fmeta(self, path: str) -> Optional[FileRecord]: assert (f:=self.get_meta(path)) is None or isinstance(f, FileRecord); return f
|
228
|
+
def get_dmeta(self, path: str) -> Optional[DirectoryRecord]: assert (d:=self.get_meta(path)) is None or isinstance(d, DirectoryRecord); return d
|
226
229
|
|
227
230
|
def list_path(self, path: str) -> PathContents:
|
228
231
|
"""
|
lfss/cli/cli.py
CHANGED
@@ -126,7 +126,7 @@ def main():
|
|
126
126
|
elif args.command == "query":
|
127
127
|
for path in args.path:
|
128
128
|
with catch_request_error():
|
129
|
-
res = connector.
|
129
|
+
res = connector.get_meta(path)
|
130
130
|
if res is None:
|
131
131
|
print(f"\033[31mNot found\033[0m ({path})")
|
132
132
|
else:
|
lfss/eng/database.py
CHANGED
@@ -1062,7 +1062,7 @@ async def _get_path_owner(cur: aiosqlite.Cursor, path: str) -> UserRecord:
|
|
1062
1062
|
uconn = UserConn(cur)
|
1063
1063
|
path_user = await uconn.get_user(path_username)
|
1064
1064
|
if path_user is None:
|
1065
|
-
raise
|
1065
|
+
raise InvalidPathError(f"Invalid path: {path_username} is not a valid username")
|
1066
1066
|
return path_user
|
1067
1067
|
|
1068
1068
|
async def check_file_read_permission(user: UserRecord, file: FileRecord, cursor: Optional[aiosqlite.Cursor] = None) -> tuple[bool, str]:
|
@@ -1111,12 +1111,6 @@ async def check_path_permission(path: str, user: UserRecord, cursor: Optional[ai
|
|
1111
1111
|
If the path is a file, the user will have all access if the user is the owner.
|
1112
1112
|
Otherwise, the user will have alias level access w.r.t. the path user.
|
1113
1113
|
"""
|
1114
|
-
if user.id == 0:
|
1115
|
-
return AccessLevel.GUEST
|
1116
|
-
|
1117
|
-
if user.is_admin:
|
1118
|
-
return AccessLevel.ALL
|
1119
|
-
|
1120
1114
|
@asynccontextmanager
|
1121
1115
|
async def this_cur():
|
1122
1116
|
if cursor is None:
|
@@ -1125,10 +1119,16 @@ async def check_path_permission(path: str, user: UserRecord, cursor: Optional[ai
|
|
1125
1119
|
else:
|
1126
1120
|
yield cursor
|
1127
1121
|
|
1128
|
-
# check if path user exists
|
1122
|
+
# check if path user exists, may raise exception
|
1129
1123
|
async with this_cur() as cur:
|
1130
1124
|
path_owner = await _get_path_owner(cur, path)
|
1131
1125
|
|
1126
|
+
if user.id == 0:
|
1127
|
+
return AccessLevel.GUEST
|
1128
|
+
|
1129
|
+
if user.is_admin:
|
1130
|
+
return AccessLevel.ALL
|
1131
|
+
|
1132
1132
|
# check if user is admin or the owner of the path
|
1133
1133
|
if user.id == path_owner.id:
|
1134
1134
|
return AccessLevel.ALL
|
@@ -12,17 +12,17 @@ frontend/login.css,sha256=VMM0QfbDFYerxKWKSGhMI1yg5IRBXg0TTdLJEEhQZNk,355
|
|
12
12
|
frontend/login.js,sha256=QoO8yKmBHDVP-ZomCMOaV7xVUVIhpl7esJrb6T5aHQE,2466
|
13
13
|
frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
|
14
14
|
frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
|
15
|
-
frontend/scripts.js,sha256=
|
15
|
+
frontend/scripts.js,sha256=OAx6o3Aabx-cE41uBABP62myZM8WbLxY37uXITMl8nY,24204
|
16
16
|
frontend/state.js,sha256=vbNL5DProRKmSEY7xu9mZH6IY0PBenF8WGxPtGgDnLI,1680
|
17
17
|
frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
|
18
18
|
frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
|
19
19
|
frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
|
20
|
-
frontend/utils.js,sha256=
|
21
|
-
lfss/api/__init__.py,sha256=
|
22
|
-
lfss/api/connector.py,sha256=
|
20
|
+
frontend/utils.js,sha256=jqAZ7Xhlk8ZI97BRnd1dpFJcW0kPrN216xSFnrTT6zk,6069
|
21
|
+
lfss/api/__init__.py,sha256=zT1JCiUM76wX-GtRrmKhTUzSYYfcmoyI1vYwN0fCcLw,6818
|
22
|
+
lfss/api/connector.py,sha256=xl_WrvupplepZSYJs4pN9zN7GDnuZR2A8-pc08ILutI,13231
|
23
23
|
lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
|
24
24
|
lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
|
25
|
-
lfss/cli/cli.py,sha256=
|
25
|
+
lfss/cli/cli.py,sha256=tPeUgj0BR_M649AGcBYwfsrGioes0qzGc0lghFkrjoo,8086
|
26
26
|
lfss/cli/panel.py,sha256=Xq3I_n-ctveym-Gh9LaUpzHiLlvt3a_nuDiwUS-MGrg,1597
|
27
27
|
lfss/cli/serve.py,sha256=vTo6_BiD7Dn3VLvHsC5RKRBC3lMu45JVr_0SqpgHdj0,1086
|
28
28
|
lfss/cli/user.py,sha256=1mTroQbaKxHjFCPHT67xwd08v-zxH0RZ_OnVc-4MzL0,5364
|
@@ -31,7 +31,7 @@ lfss/eng/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
lfss/eng/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
|
32
32
|
lfss/eng/config.py,sha256=FcTtPL7bOpg54nVL_gX-VTIjfN1cafy423ezoWGvouY,874
|
33
33
|
lfss/eng/connection_pool.py,sha256=1aq7nSgd7hB9YNV4PjD1RDRyl_moDw3ubBtSLyfgGBs,6320
|
34
|
-
lfss/eng/database.py,sha256
|
34
|
+
lfss/eng/database.py,sha256=-6-IgR6hXe4ouMH8e0Ryeh2gZXJBpna1ech41sZ3UYs,53267
|
35
35
|
lfss/eng/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
|
36
36
|
lfss/eng/error.py,sha256=JGf5NV-f4rL6tNIDSAx5-l9MG8dEj7F2w_MuOjj1d1o,732
|
37
37
|
lfss/eng/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
|
@@ -45,7 +45,7 @@ lfss/svc/app_dav.py,sha256=D0KSgjtTktPjIhyIKG5eRmBdh5X8HYFYH151E6gzlbc,18245
|
|
45
45
|
lfss/svc/app_native.py,sha256=JbPge-F9irl26tXKAzfA5DfyjCh0Dgttflztqqrvt0A,8890
|
46
46
|
lfss/svc/common_impl.py,sha256=5ZRM24zVZpAeipgDtZUVBMFtArkydlAkn17ic_XL7v8,13733
|
47
47
|
lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
|
48
|
-
lfss-0.11.
|
49
|
-
lfss-0.11.
|
50
|
-
lfss-0.11.
|
51
|
-
lfss-0.11.
|
48
|
+
lfss-0.11.1.dist-info/METADATA,sha256=qXJcsBI6dboEavUMZcRuCQFLzQ8i5cUqWg5OJWrTr8k,2712
|
49
|
+
lfss-0.11.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
50
|
+
lfss-0.11.1.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
|
51
|
+
lfss-0.11.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|