lfss 0.7.15__py3-none-any.whl → 0.8.0__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.
- Readme.md +2 -2
- docs/Permission.md +4 -2
- frontend/api.js +214 -8
- frontend/index.html +40 -28
- frontend/login.css +21 -0
- frontend/login.js +83 -0
- frontend/scripts.js +73 -84
- frontend/state.js +19 -4
- frontend/styles.css +26 -8
- frontend/thumb.css +6 -0
- frontend/thumb.js +6 -2
- lfss/{client → api}/__init__.py +52 -35
- lfss/{client/api.py → api/connector.py} +89 -8
- lfss/cli/cli.py +1 -1
- lfss/cli/user.py +1 -1
- lfss/src/connection_pool.py +3 -2
- lfss/src/database.py +158 -72
- lfss/src/datatype.py +8 -3
- lfss/src/error.py +3 -1
- lfss/src/server.py +67 -9
- lfss/src/stat.py +1 -1
- lfss/src/utils.py +47 -13
- {lfss-0.7.15.dist-info → lfss-0.8.0.dist-info}/METADATA +4 -3
- lfss-0.8.0.dist-info/RECORD +43 -0
- lfss-0.7.15.dist-info/RECORD +0 -41
- {lfss-0.7.15.dist-info → lfss-0.8.0.dist-info}/WHEEL +0 -0
- {lfss-0.7.15.dist-info → lfss-0.8.0.dist-info}/entry_points.txt +0 -0
frontend/scripts.js
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
import { permMap } from './api.js';
|
1
|
+
import { permMap, listPath } from './api.js';
|
2
2
|
import { showFloatingWindowLineInput, showPopup } from './popup.js';
|
3
3
|
import { formatSize, decodePathURI, ensurePathURI, getRandomString, cvtGMT2Local, debounce, encodePathURI, asHtmlText } from './utils.js';
|
4
4
|
import { showInfoPanel, showDirInfoPanel } from './info.js';
|
5
5
|
import { makeThumbHtml } from './thumb.js';
|
6
6
|
import { store } from './state.js';
|
7
|
+
import { maybeShowLoginPanel } from './login.js';
|
7
8
|
|
8
9
|
/** @type {import('./api.js').UserRecord}*/
|
9
10
|
let userRecord = null;
|
@@ -12,8 +13,6 @@ const ensureSlashEnd = (path) => {
|
|
12
13
|
return path.endsWith('/') ? path : path + '/';
|
13
14
|
}
|
14
15
|
|
15
|
-
const endpointInput = document.querySelector('input#endpoint');
|
16
|
-
const tokenInput = document.querySelector('input#token');
|
17
16
|
const pathInput = document.querySelector('input#path');
|
18
17
|
const pathBackButton = document.querySelector('span#back-btn');
|
19
18
|
const pathHintDiv = document.querySelector('#position-hint');
|
@@ -24,42 +23,49 @@ const uploadFileSelector = document.querySelector('#file-selector');
|
|
24
23
|
const uploadFileNameInput = document.querySelector('#file-name');
|
25
24
|
const uploadButton = document.querySelector('#upload-btn');
|
26
25
|
const randomizeFnameButton = document.querySelector('#randomize-fname-btn');
|
26
|
+
const pageLimitSelect = document.querySelector('#page-limit-sel');
|
27
|
+
const pageNumInput = document.querySelector('#page-num-input');
|
28
|
+
const pageCountLabel = document.querySelector('#page-count-lbl');
|
27
29
|
const sortBySelect = document.querySelector('#sort-by-sel');
|
28
30
|
const sortOrderSelect = document.querySelector('#sort-order-sel');
|
29
31
|
|
30
32
|
const conn = store.conn;
|
31
|
-
store.init();
|
32
33
|
|
33
34
|
{
|
34
|
-
|
35
|
-
|
35
|
+
// initialization
|
36
|
+
store.init();
|
36
37
|
pathInput.value = store.dirpath;
|
37
38
|
uploadFilePrefixLabel.textContent = pathInput.value;
|
38
|
-
sortBySelect.value = store.
|
39
|
+
sortBySelect.value = store.orderby;
|
39
40
|
sortOrderSelect.value = store.sortorder;
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
pageLimitSelect.value = store.pagelim;
|
42
|
+
pageNumInput.value = store.pagenum;
|
43
|
+
|
44
|
+
maybeShowLoginPanel(store,).then(
|
45
|
+
(user) => {
|
46
|
+
console.log("User record", user);
|
47
|
+
userRecord = user;
|
48
|
+
maybeRefreshFileList();
|
49
|
+
}
|
50
|
+
)
|
43
51
|
}
|
44
52
|
|
53
|
+
pathHintDiv.addEventListener('click', () => {
|
54
|
+
maybeShowLoginPanel(store, true).then(
|
55
|
+
(user) => {
|
56
|
+
console.log("User record", user);
|
57
|
+
userRecord = user;
|
58
|
+
maybeRefreshFileList();
|
59
|
+
}
|
60
|
+
);
|
61
|
+
});
|
62
|
+
|
45
63
|
function onPathChange(){
|
46
64
|
uploadFilePrefixLabel.textContent = pathInput.value;
|
47
65
|
store.dirpath = pathInput.value;
|
48
66
|
maybeRefreshFileList();
|
49
67
|
}
|
50
68
|
|
51
|
-
endpointInput.addEventListener('blur', () => {
|
52
|
-
store.endpoint = endpointInput.value;
|
53
|
-
maybeRefreshUserRecord().then(
|
54
|
-
() => maybeRefreshFileList()
|
55
|
-
);
|
56
|
-
});
|
57
|
-
tokenInput.addEventListener('blur', () => {
|
58
|
-
store.token = tokenInput.value;
|
59
|
-
maybeRefreshUserRecord().then(
|
60
|
-
() => maybeRefreshFileList()
|
61
|
-
);
|
62
|
-
});
|
63
69
|
pathInput.addEventListener('input', () => {
|
64
70
|
onPathChange();
|
65
71
|
});
|
@@ -212,54 +218,63 @@ function maybeRefreshFileList(){
|
|
212
218
|
}
|
213
219
|
}
|
214
220
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
function sortFileList(files){
|
224
|
-
function timestr2num(timestr){
|
225
|
-
return new Date(timestr).getTime();
|
226
|
-
}
|
227
|
-
if (store.sortby === 'name'){
|
228
|
-
files.sort((a, b) => { return a.url.localeCompare(b.url); });
|
229
|
-
}
|
230
|
-
if (store.sortby === 'size'){
|
231
|
-
files.sort((a, b) => { return a.file_size - b.file_size; });
|
232
|
-
}
|
233
|
-
if (store.sortby === 'access'){
|
234
|
-
files.sort((a, b) => { return timestr2num(a.access_time) - timestr2num(b.access_time); });
|
221
|
+
sortBySelect.addEventListener('change', (elem) => {store.orderby = elem.target.value; refreshFileList();});
|
222
|
+
sortOrderSelect.addEventListener('change', (elem) => {store.sortorder = elem.target.value; refreshFileList();});
|
223
|
+
pageLimitSelect.addEventListener('change', (elem) => {store.pagelim = elem.target.value; refreshFileList();});
|
224
|
+
pageNumInput.addEventListener('change', (elem) => {store.pagenum = elem.target.value; refreshFileList();});
|
225
|
+
|
226
|
+
window.addEventListener('keydown', (e) => {
|
227
|
+
if (document.activeElement !== document.body){
|
228
|
+
return;
|
235
229
|
}
|
236
|
-
if (
|
237
|
-
|
230
|
+
if (e.key === 'ArrowLeft'){
|
231
|
+
const num = Math.max(store.pagenum - 1, 1);
|
232
|
+
pageNumInput.value = num;
|
233
|
+
store.pagenum = num;
|
234
|
+
refreshFileList();
|
238
235
|
}
|
239
|
-
if (
|
240
|
-
|
236
|
+
else if (e.key === 'ArrowRight'){
|
237
|
+
const num = Math.min(Math.max(store.pagenum + 1, 1), parseInt(pageCountLabel.textContent));
|
238
|
+
pageNumInput.value = num;
|
239
|
+
store.pagenum = num;
|
240
|
+
refreshFileList();
|
241
241
|
}
|
242
|
-
|
243
|
-
}
|
244
|
-
sortBySelect.addEventListener('change', (elem) => {store.sortby = elem.target.value; refreshFileList();});
|
245
|
-
sortOrderSelect.addEventListener('change', (elem) => {store.sortorder = elem.target.value; refreshFileList();});
|
242
|
+
})
|
246
243
|
|
247
|
-
function refreshFileList(){
|
248
|
-
|
249
|
-
|
244
|
+
async function refreshFileList(){
|
245
|
+
|
246
|
+
listPath(conn, store.dirpath, {
|
247
|
+
offset: (store.pagenum - 1) * store.pagelim,
|
248
|
+
limit: store.pagelim,
|
249
|
+
orderBy: store.orderby,
|
250
|
+
orderDesc: store.sortorder === 'desc'
|
251
|
+
})
|
252
|
+
.then(async (res) => {
|
250
253
|
pathHintDiv.classList.remove('disconnected');
|
251
254
|
pathHintDiv.classList.add('connected');
|
252
|
-
pathHintLabel.textContent = store.dirpath
|
255
|
+
pathHintLabel.textContent = `[${userRecord.username}] ${store.endpoint}/${store.dirpath.startsWith('/') ? store.dirpath.slice(1) : store.dirpath}`;
|
253
256
|
tbody.innerHTML = '';
|
257
|
+
console.log("Got data", res);
|
254
258
|
|
255
|
-
|
259
|
+
const [data, count] = res;
|
256
260
|
|
261
|
+
{
|
262
|
+
const total = count.dirs + count.files;
|
263
|
+
const pageCount = Math.max(Math.ceil(total / store.pagelim), 1);
|
264
|
+
pageCountLabel.textContent = pageCount;
|
265
|
+
if (store.pagenum > pageCount){
|
266
|
+
store.pagenum = pageCount;
|
267
|
+
pageNumInput.value = pageCount;
|
268
|
+
|
269
|
+
await refreshFileList();
|
270
|
+
return;
|
271
|
+
}
|
272
|
+
}
|
273
|
+
|
274
|
+
// maybe undefined
|
257
275
|
if (!data.dirs){ data.dirs = []; }
|
258
276
|
if (!data.files){ data.files = []; }
|
259
277
|
|
260
|
-
sortDirList(data.dirs);
|
261
|
-
sortFileList(data.files);
|
262
|
-
|
263
278
|
data.dirs.forEach(dir => {
|
264
279
|
const tr = document.createElement('tr');
|
265
280
|
const sizeTd = document.createElement('td');
|
@@ -448,12 +463,6 @@ function refreshFileList(){
|
|
448
463
|
});
|
449
464
|
actContainer.appendChild(infoButton);
|
450
465
|
|
451
|
-
const viewButton = document.createElement('a');
|
452
|
-
viewButton.textContent = 'View';
|
453
|
-
viewButton.href = conn.config.endpoint + '/' + file.url + '?token=' + conn.config.token;
|
454
|
-
viewButton.target = '_blank';
|
455
|
-
actContainer.appendChild(viewButton);
|
456
|
-
|
457
466
|
const moveButton = document.createElement('a');
|
458
467
|
moveButton.textContent = 'Move';
|
459
468
|
moveButton.style.cursor = 'pointer';
|
@@ -516,24 +525,4 @@ function refreshFileList(){
|
|
516
525
|
);
|
517
526
|
}
|
518
527
|
|
519
|
-
|
520
|
-
async function maybeRefreshUserRecord(){
|
521
|
-
if (store.endpoint && store.token){
|
522
|
-
await refreshUserRecord();
|
523
|
-
}
|
524
|
-
}
|
525
|
-
|
526
|
-
async function refreshUserRecord(){
|
527
|
-
try{
|
528
|
-
userRecord = await conn.whoami();
|
529
|
-
console.log("User record: ", userRecord);
|
530
|
-
}
|
531
|
-
catch (err){
|
532
|
-
userRecord = null;
|
533
|
-
console.error("Failed to get user record");
|
534
|
-
return false;
|
535
|
-
}
|
536
|
-
return true;
|
537
|
-
}
|
538
|
-
|
539
528
|
console.log("Hello World");
|
frontend/state.js
CHANGED
@@ -41,11 +41,11 @@ export const store = {
|
|
41
41
|
setPersistedState('dirpath', pth);
|
42
42
|
},
|
43
43
|
|
44
|
-
get
|
45
|
-
return loadPersistedState('
|
44
|
+
get orderby () {
|
45
|
+
return loadPersistedState('orderby', 'none');
|
46
46
|
},
|
47
|
-
set
|
48
|
-
setPersistedState('
|
47
|
+
set orderby (sb) {
|
48
|
+
setPersistedState('orderby', sb);
|
49
49
|
},
|
50
50
|
|
51
51
|
get sortorder () {
|
@@ -54,4 +54,19 @@ export const store = {
|
|
54
54
|
set sortorder (so) {
|
55
55
|
setPersistedState('sortorder', so);
|
56
56
|
},
|
57
|
+
|
58
|
+
get pagenum () {
|
59
|
+
return parseInt(loadPersistedState('pagenum', '1'));
|
60
|
+
},
|
61
|
+
set pagenum (pn) {
|
62
|
+
setPersistedState('pagenum', pn.toString());
|
63
|
+
},
|
64
|
+
|
65
|
+
get pagelim () {
|
66
|
+
return parseInt(loadPersistedState('pagelim', '100'));
|
67
|
+
},
|
68
|
+
set pagelim (ps) {
|
69
|
+
setPersistedState('pagelim', ps.toString());
|
70
|
+
},
|
71
|
+
|
57
72
|
};
|
frontend/styles.css
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
@import "./popup.css";
|
2
2
|
@import "./info.css";
|
3
3
|
@import "./thumb.css";
|
4
|
+
@import "./login.css";
|
5
|
+
|
6
|
+
html {
|
7
|
+
overflow: -moz-scrollbars-vertical;
|
8
|
+
overflow-y: scroll;
|
9
|
+
}
|
4
10
|
|
5
11
|
body{
|
6
12
|
font-family: Arial, sans-serif;
|
@@ -44,7 +50,9 @@ div.container.header, div.container.footer{
|
|
44
50
|
|
45
51
|
div.container.header{
|
46
52
|
top: 0;
|
47
|
-
height:
|
53
|
+
height: 6.5rem;
|
54
|
+
justify-content: flex-end;
|
55
|
+
gap: 0.2rem;
|
48
56
|
}
|
49
57
|
div.container.footer{
|
50
58
|
bottom: 0;
|
@@ -54,7 +62,7 @@ div.container.footer{
|
|
54
62
|
div.container.content{
|
55
63
|
width: 100%;
|
56
64
|
padding-inline: 0.5rem;
|
57
|
-
margin-top: calc(
|
65
|
+
margin-top: calc(6.5rem + 1rem);
|
58
66
|
margin-bottom: calc(4rem + 0.5rem);
|
59
67
|
}
|
60
68
|
|
@@ -122,13 +130,22 @@ input#path{
|
|
122
130
|
}
|
123
131
|
|
124
132
|
div#top-bar{
|
133
|
+
color: grey;
|
134
|
+
display: flex;
|
135
|
+
flex-direction: row;
|
136
|
+
width: calc(100% - 2rem);
|
137
|
+
}
|
138
|
+
div#settings-bar{
|
139
|
+
width: calc(100% - 2rem);
|
125
140
|
display: flex;
|
126
141
|
flex-direction: row;
|
127
142
|
justify-content: space-between;
|
128
143
|
align-items: center;
|
129
144
|
gap: 1rem;
|
145
|
+
padding-bottom: 0.2rem;
|
130
146
|
}
|
131
147
|
div#position-hint{
|
148
|
+
cursor: pointer;
|
132
149
|
color: rgb(138, 138, 138);
|
133
150
|
border-radius: 0.5rem;
|
134
151
|
display: flex;
|
@@ -137,8 +154,15 @@ div#position-hint{
|
|
137
154
|
gap: 0.25rem;
|
138
155
|
height: 1rem;
|
139
156
|
margin-bottom: 0.25rem;
|
157
|
+
font-size: x-small;
|
140
158
|
|
141
159
|
padding: 0.25rem;
|
160
|
+
transition: all 0.2s;
|
161
|
+
}
|
162
|
+
div#position-hint:hover{
|
163
|
+
background-color: rgb(240, 244, 246);
|
164
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
165
|
+
color:rgb(91, 107, 116)
|
142
166
|
}
|
143
167
|
div#position-hint span{
|
144
168
|
width: 0.75rem;
|
@@ -152,12 +176,6 @@ div#position-hint.connected span{
|
|
152
176
|
background-color: rgb(0, 166, 0);
|
153
177
|
}
|
154
178
|
|
155
|
-
div#settings {
|
156
|
-
display: flex;
|
157
|
-
flex-direction: column;
|
158
|
-
gap: 10px;
|
159
|
-
}
|
160
|
-
|
161
179
|
label#bucket-label{
|
162
180
|
color: #195f8b;
|
163
181
|
}
|
frontend/thumb.css
CHANGED
frontend/thumb.js
CHANGED
@@ -72,14 +72,18 @@ export function makeThumbHtml(c, r){
|
|
72
72
|
return `
|
73
73
|
<div class="thumb" id="${thumb_id}"> \
|
74
74
|
<img src="${c.config.endpoint}/${url}?token=${token}&thumb=true" alt="${r.url}" class="thumb" \
|
75
|
-
onerror="this.src='${getSafeIconUrl(getIconSVGFromMimeType(mtype))}';this.classList.add('thumb-svg');"
|
75
|
+
onerror="this.src='${getSafeIconUrl(getIconSVGFromMimeType(mtype))}';this.classList.add('thumb-svg');" \
|
76
|
+
onclick="window.open('${c.config.endpoint}/${url}?token=${token}', '_blank');" \
|
77
|
+
/> \
|
76
78
|
</div>
|
77
79
|
`;
|
78
80
|
}
|
79
81
|
else{
|
80
82
|
return `
|
81
83
|
<div class="thumb" id="${thumb_id}"> \
|
82
|
-
<img src="${getSafeIconUrl(getIconSVGFromMimeType(mtype))}" alt="${r.url}" class="thumb thumb-svg"
|
84
|
+
<img src="${getSafeIconUrl(getIconSVGFromMimeType(mtype))}" alt="${r.url}" class="thumb thumb-svg" \
|
85
|
+
onclick="window.open('${c.config.endpoint}/${url}?token=${token}', '_blank');" \
|
86
|
+
/ > \
|
83
87
|
</div>
|
84
88
|
`;
|
85
89
|
}
|
lfss/{client → api}/__init__.py
RENAMED
@@ -1,6 +1,8 @@
|
|
1
1
|
import os, time, pathlib
|
2
2
|
from threading import Lock
|
3
|
-
from .
|
3
|
+
from .connector import Connector
|
4
|
+
from ..src.datatype import FileRecord
|
5
|
+
from ..src.utils import decode_uri_compnents
|
4
6
|
from ..src.bounded_pool import BoundedThreadPoolExecutor
|
5
7
|
|
6
8
|
def upload_file(
|
@@ -11,8 +13,9 @@ def upload_file(
|
|
11
13
|
interval: float = 0,
|
12
14
|
verbose: bool = False,
|
13
15
|
**put_kwargs
|
14
|
-
):
|
16
|
+
) -> tuple[bool, str]:
|
15
17
|
this_try = 0
|
18
|
+
error_msg = ""
|
16
19
|
while this_try <= n_retries:
|
17
20
|
try:
|
18
21
|
with open(file_path, 'rb') as f:
|
@@ -24,6 +27,7 @@ def upload_file(
|
|
24
27
|
raise e
|
25
28
|
if verbose:
|
26
29
|
print(f"Error uploading {file_path}: {e}, retrying...")
|
30
|
+
error_msg = str(e)
|
27
31
|
this_try += 1
|
28
32
|
finally:
|
29
33
|
time.sleep(interval)
|
@@ -31,8 +35,8 @@ def upload_file(
|
|
31
35
|
if this_try > n_retries:
|
32
36
|
if verbose:
|
33
37
|
print(f"Failed to upload {file_path} after {n_retries} retries.")
|
34
|
-
return False
|
35
|
-
return True
|
38
|
+
return False, error_msg
|
39
|
+
return True, error_msg
|
36
40
|
|
37
41
|
def upload_directory(
|
38
42
|
connector: Connector,
|
@@ -43,7 +47,7 @@ def upload_directory(
|
|
43
47
|
interval: float = 0,
|
44
48
|
verbose: bool = False,
|
45
49
|
**put_kwargs
|
46
|
-
) -> list[str]:
|
50
|
+
) -> list[tuple[str, str]]:
|
47
51
|
assert path.endswith('/'), "Path must end with a slash."
|
48
52
|
if path.startswith('/'):
|
49
53
|
path = path[1:]
|
@@ -52,8 +56,8 @@ def upload_directory(
|
|
52
56
|
_counter = 0
|
53
57
|
_counter_lock = Lock()
|
54
58
|
|
55
|
-
|
56
|
-
def put_file(file_path):
|
59
|
+
faild_items = []
|
60
|
+
def put_file(c: Connector, file_path):
|
57
61
|
with _counter_lock:
|
58
62
|
nonlocal _counter
|
59
63
|
_counter += 1
|
@@ -62,18 +66,19 @@ def upload_directory(
|
|
62
66
|
if verbose:
|
63
67
|
print(f"[{this_count}] Uploading {file_path} to {dst_path}")
|
64
68
|
|
65
|
-
if not upload_file(
|
66
|
-
|
69
|
+
if not (res:=upload_file(
|
70
|
+
c, file_path, dst_path,
|
67
71
|
n_retries=n_retries, interval=interval, verbose=verbose, **put_kwargs
|
68
|
-
):
|
69
|
-
|
72
|
+
))[0]:
|
73
|
+
faild_items.append((file_path, res[1]))
|
70
74
|
|
71
|
-
with
|
72
|
-
|
73
|
-
for
|
74
|
-
|
75
|
+
with connector.session(n_concurrent) as c:
|
76
|
+
with BoundedThreadPoolExecutor(n_concurrent) as executor:
|
77
|
+
for root, dirs, files in os.walk(directory):
|
78
|
+
for file in files:
|
79
|
+
executor.submit(put_file, c, os.path.join(root, file))
|
75
80
|
|
76
|
-
return
|
81
|
+
return faild_items
|
77
82
|
|
78
83
|
def download_file(
|
79
84
|
connector: Connector,
|
@@ -83,17 +88,19 @@ def download_file(
|
|
83
88
|
interval: float = 0,
|
84
89
|
verbose: bool = False,
|
85
90
|
overwrite: bool = False
|
86
|
-
):
|
91
|
+
) -> tuple[bool, str]:
|
87
92
|
this_try = 0
|
93
|
+
error_msg = ""
|
88
94
|
while this_try <= n_retries:
|
89
95
|
if not overwrite and os.path.exists(file_path):
|
90
96
|
if verbose:
|
91
97
|
print(f"File {file_path} already exists, skipping download.")
|
92
|
-
return True
|
98
|
+
return True, error_msg
|
93
99
|
try:
|
94
100
|
blob = connector.get(src_url)
|
95
|
-
if
|
96
|
-
|
101
|
+
if blob is None:
|
102
|
+
error_msg = "File not found."
|
103
|
+
return False, error_msg
|
97
104
|
pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
98
105
|
with open(file_path, 'wb') as f:
|
99
106
|
f.write(blob)
|
@@ -103,6 +110,7 @@ def download_file(
|
|
103
110
|
raise e
|
104
111
|
if verbose:
|
105
112
|
print(f"Error downloading {src_url}: {e}, retrying...")
|
113
|
+
error_msg = str(e)
|
106
114
|
this_try += 1
|
107
115
|
finally:
|
108
116
|
time.sleep(interval)
|
@@ -110,8 +118,8 @@ def download_file(
|
|
110
118
|
if this_try > n_retries:
|
111
119
|
if verbose:
|
112
120
|
print(f"Failed to download {src_url} after {n_retries} retries.")
|
113
|
-
return False
|
114
|
-
return True
|
121
|
+
return False, error_msg
|
122
|
+
return True, error_msg
|
115
123
|
|
116
124
|
def download_directory(
|
117
125
|
connector: Connector,
|
@@ -122,7 +130,7 @@ def download_directory(
|
|
122
130
|
interval: float = 0,
|
123
131
|
verbose: bool = False,
|
124
132
|
overwrite: bool = False
|
125
|
-
) -> list[str]:
|
133
|
+
) -> list[tuple[str, str]]:
|
126
134
|
|
127
135
|
directory = str(directory)
|
128
136
|
|
@@ -133,23 +141,32 @@ def download_directory(
|
|
133
141
|
|
134
142
|
_counter = 0
|
135
143
|
_counter_lock = Lock()
|
136
|
-
|
137
|
-
def get_file(src_url):
|
138
|
-
nonlocal _counter,
|
144
|
+
failed_items: list[tuple[str, str]] = []
|
145
|
+
def get_file(c, src_url):
|
146
|
+
nonlocal _counter, failed_items
|
139
147
|
with _counter_lock:
|
140
148
|
_counter += 1
|
141
149
|
this_count = _counter
|
142
|
-
dst_path = f"{directory}{os.path.relpath(src_url, src_path)}"
|
150
|
+
dst_path = f"{directory}{os.path.relpath(decode_uri_compnents(src_url), decode_uri_compnents(src_path))}"
|
143
151
|
if verbose:
|
144
152
|
print(f"[{this_count}] Downloading {src_url} to {dst_path}")
|
145
153
|
|
146
|
-
if not download_file(
|
147
|
-
|
154
|
+
if not (res:=download_file(
|
155
|
+
c, src_url, dst_path,
|
148
156
|
n_retries=n_retries, interval=interval, verbose=verbose, overwrite=overwrite
|
149
|
-
):
|
150
|
-
|
157
|
+
))[0]:
|
158
|
+
failed_items.append((src_url, res[1]))
|
151
159
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
160
|
+
batch_size = 10000
|
161
|
+
file_list: list[FileRecord] = []
|
162
|
+
with connector.session(n_concurrent) as c:
|
163
|
+
file_count = c.count_files(src_path, flat=True)
|
164
|
+
for offset in range(0, file_count, batch_size):
|
165
|
+
file_list.extend(c.list_files(
|
166
|
+
src_path, offset=offset, limit=batch_size, flat=True
|
167
|
+
))
|
168
|
+
|
169
|
+
with BoundedThreadPoolExecutor(n_concurrent) as executor:
|
170
|
+
for file in file_list:
|
171
|
+
executor.submit(get_file, c, file.url)
|
172
|
+
return failed_items
|