lfss 0.7.15__py3-none-any.whl → 0.8.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.
- Readme.md +2 -2
- docs/Permission.md +4 -2
- frontend/api.js +271 -8
- frontend/index.html +40 -28
- frontend/login.css +21 -0
- frontend/login.js +83 -0
- frontend/scripts.js +77 -88
- 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 +72 -41
- lfss/api/connector.py +261 -0
- lfss/cli/cli.py +1 -1
- lfss/cli/user.py +1 -1
- lfss/src/config.py +1 -1
- lfss/src/connection_pool.py +3 -2
- lfss/src/database.py +193 -100
- lfss/src/datatype.py +8 -3
- lfss/src/error.py +3 -1
- lfss/src/server.py +147 -61
- lfss/src/stat.py +1 -1
- lfss/src/utils.py +47 -13
- {lfss-0.7.15.dist-info → lfss-0.8.1.dist-info}/METADATA +5 -3
- lfss-0.8.1.dist-info/RECORD +43 -0
- lfss/client/api.py +0 -143
- lfss-0.7.15.dist-info/RECORD +0 -41
- {lfss-0.7.15.dist-info → lfss-0.8.1.dist-info}/WHEEL +0 -0
- {lfss-0.7.15.dist-info → lfss-0.8.1.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, uploadFile } 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
|
});
|
@@ -126,7 +132,7 @@ uploadButton.addEventListener('click', () => {
|
|
126
132
|
}
|
127
133
|
path = path + fileName;
|
128
134
|
showPopup('Uploading...', {level: 'info', timeout: 3000});
|
129
|
-
conn
|
135
|
+
uploadFile(conn, path, file, {'conflict': 'overwrite'})
|
130
136
|
.then(() => {
|
131
137
|
refreshFileList();
|
132
138
|
uploadFileNameInput.value = '';
|
@@ -172,10 +178,10 @@ Are you sure you want to proceed?
|
|
172
178
|
`)){ return; }
|
173
179
|
|
174
180
|
let counter = 0;
|
175
|
-
async function
|
181
|
+
async function uploadFileFn(...args){
|
176
182
|
const [file, path] = args;
|
177
183
|
try{
|
178
|
-
await conn
|
184
|
+
await uploadFile(conn, path, file, {conflict: 'overwrite'});
|
179
185
|
}
|
180
186
|
catch (err){
|
181
187
|
showPopup('Failed to upload file [' + file.name + ']: ' + err, {level: 'error', timeout: 5000});
|
@@ -188,7 +194,7 @@ Are you sure you want to proceed?
|
|
188
194
|
for (let i = 0; i < files.length; i++){
|
189
195
|
const file = files[i];
|
190
196
|
const path = dstPath + file.name;
|
191
|
-
promises.push(
|
197
|
+
promises.push(uploadFileFn(file, path));
|
192
198
|
}
|
193
199
|
showPopup('Uploading multiple files...', {level: 'info', timeout: 3000});
|
194
200
|
Promise.all(promises).then(
|
@@ -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,19 +13,25 @@ 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
|
-
|
19
|
-
|
20
|
-
|
21
|
+
fsize = os.path.getsize(file_path)
|
22
|
+
if fsize < 32 * 1024 * 1024: # 32MB
|
23
|
+
with open(file_path, 'rb') as f:
|
24
|
+
blob = f.read()
|
25
|
+
connector.put(dst_url, blob, **put_kwargs)
|
26
|
+
else:
|
27
|
+
connector.post(dst_url, file_path, **put_kwargs)
|
21
28
|
break
|
22
29
|
except Exception as e:
|
23
30
|
if isinstance(e, KeyboardInterrupt):
|
24
31
|
raise e
|
25
32
|
if verbose:
|
26
33
|
print(f"Error uploading {file_path}: {e}, retrying...")
|
34
|
+
error_msg = str(e)
|
27
35
|
this_try += 1
|
28
36
|
finally:
|
29
37
|
time.sleep(interval)
|
@@ -31,8 +39,8 @@ def upload_file(
|
|
31
39
|
if this_try > n_retries:
|
32
40
|
if verbose:
|
33
41
|
print(f"Failed to upload {file_path} after {n_retries} retries.")
|
34
|
-
return False
|
35
|
-
return True
|
42
|
+
return False, error_msg
|
43
|
+
return True, error_msg
|
36
44
|
|
37
45
|
def upload_directory(
|
38
46
|
connector: Connector,
|
@@ -43,7 +51,7 @@ def upload_directory(
|
|
43
51
|
interval: float = 0,
|
44
52
|
verbose: bool = False,
|
45
53
|
**put_kwargs
|
46
|
-
) -> list[str]:
|
54
|
+
) -> list[tuple[str, str]]:
|
47
55
|
assert path.endswith('/'), "Path must end with a slash."
|
48
56
|
if path.startswith('/'):
|
49
57
|
path = path[1:]
|
@@ -52,8 +60,8 @@ def upload_directory(
|
|
52
60
|
_counter = 0
|
53
61
|
_counter_lock = Lock()
|
54
62
|
|
55
|
-
|
56
|
-
def put_file(file_path):
|
63
|
+
faild_items = []
|
64
|
+
def put_file(c: Connector, file_path):
|
57
65
|
with _counter_lock:
|
58
66
|
nonlocal _counter
|
59
67
|
_counter += 1
|
@@ -62,18 +70,19 @@ def upload_directory(
|
|
62
70
|
if verbose:
|
63
71
|
print(f"[{this_count}] Uploading {file_path} to {dst_path}")
|
64
72
|
|
65
|
-
if not upload_file(
|
66
|
-
|
73
|
+
if not (res:=upload_file(
|
74
|
+
c, file_path, dst_path,
|
67
75
|
n_retries=n_retries, interval=interval, verbose=verbose, **put_kwargs
|
68
|
-
):
|
69
|
-
|
76
|
+
))[0]:
|
77
|
+
faild_items.append((file_path, res[1]))
|
70
78
|
|
71
|
-
with
|
72
|
-
|
73
|
-
for
|
74
|
-
|
79
|
+
with connector.session(n_concurrent) as c:
|
80
|
+
with BoundedThreadPoolExecutor(n_concurrent) as executor:
|
81
|
+
for root, dirs, files in os.walk(directory):
|
82
|
+
for file in files:
|
83
|
+
executor.submit(put_file, c, os.path.join(root, file))
|
75
84
|
|
76
|
-
return
|
85
|
+
return faild_items
|
77
86
|
|
78
87
|
def download_file(
|
79
88
|
connector: Connector,
|
@@ -83,26 +92,39 @@ def download_file(
|
|
83
92
|
interval: float = 0,
|
84
93
|
verbose: bool = False,
|
85
94
|
overwrite: bool = False
|
86
|
-
):
|
95
|
+
) -> tuple[bool, str]:
|
87
96
|
this_try = 0
|
97
|
+
error_msg = ""
|
88
98
|
while this_try <= n_retries:
|
89
99
|
if not overwrite and os.path.exists(file_path):
|
90
100
|
if verbose:
|
91
101
|
print(f"File {file_path} already exists, skipping download.")
|
92
|
-
return True
|
102
|
+
return True, error_msg
|
93
103
|
try:
|
94
|
-
|
95
|
-
if
|
96
|
-
|
104
|
+
fmeta = connector.get_metadata(src_url)
|
105
|
+
if fmeta is None:
|
106
|
+
error_msg = "File not found."
|
107
|
+
return False, error_msg
|
108
|
+
|
97
109
|
pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
98
|
-
|
99
|
-
|
110
|
+
fsize = fmeta.file_size # type: ignore
|
111
|
+
if fsize < 32 * 1024 * 1024: # 32MB
|
112
|
+
blob = connector.get(src_url)
|
113
|
+
assert blob is not None
|
114
|
+
with open(file_path, 'wb') as f:
|
115
|
+
f.write(blob)
|
116
|
+
else:
|
117
|
+
with open(file_path, 'wb') as f:
|
118
|
+
for chunk in connector.get_stream(src_url):
|
119
|
+
f.write(chunk)
|
100
120
|
break
|
121
|
+
|
101
122
|
except Exception as e:
|
102
123
|
if isinstance(e, KeyboardInterrupt):
|
103
124
|
raise e
|
104
125
|
if verbose:
|
105
126
|
print(f"Error downloading {src_url}: {e}, retrying...")
|
127
|
+
error_msg = str(e)
|
106
128
|
this_try += 1
|
107
129
|
finally:
|
108
130
|
time.sleep(interval)
|
@@ -110,8 +132,8 @@ def download_file(
|
|
110
132
|
if this_try > n_retries:
|
111
133
|
if verbose:
|
112
134
|
print(f"Failed to download {src_url} after {n_retries} retries.")
|
113
|
-
return False
|
114
|
-
return True
|
135
|
+
return False, error_msg
|
136
|
+
return True, error_msg
|
115
137
|
|
116
138
|
def download_directory(
|
117
139
|
connector: Connector,
|
@@ -122,7 +144,7 @@ def download_directory(
|
|
122
144
|
interval: float = 0,
|
123
145
|
verbose: bool = False,
|
124
146
|
overwrite: bool = False
|
125
|
-
) -> list[str]:
|
147
|
+
) -> list[tuple[str, str]]:
|
126
148
|
|
127
149
|
directory = str(directory)
|
128
150
|
|
@@ -133,23 +155,32 @@ def download_directory(
|
|
133
155
|
|
134
156
|
_counter = 0
|
135
157
|
_counter_lock = Lock()
|
136
|
-
|
137
|
-
def get_file(src_url):
|
138
|
-
nonlocal _counter,
|
158
|
+
failed_items: list[tuple[str, str]] = []
|
159
|
+
def get_file(c, src_url):
|
160
|
+
nonlocal _counter, failed_items
|
139
161
|
with _counter_lock:
|
140
162
|
_counter += 1
|
141
163
|
this_count = _counter
|
142
|
-
dst_path = f"{directory}{os.path.relpath(src_url, src_path)}"
|
164
|
+
dst_path = f"{directory}{os.path.relpath(decode_uri_compnents(src_url), decode_uri_compnents(src_path))}"
|
143
165
|
if verbose:
|
144
166
|
print(f"[{this_count}] Downloading {src_url} to {dst_path}")
|
145
167
|
|
146
|
-
if not download_file(
|
147
|
-
|
168
|
+
if not (res:=download_file(
|
169
|
+
c, src_url, dst_path,
|
148
170
|
n_retries=n_retries, interval=interval, verbose=verbose, overwrite=overwrite
|
149
|
-
):
|
150
|
-
|
171
|
+
))[0]:
|
172
|
+
failed_items.append((src_url, res[1]))
|
151
173
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
174
|
+
batch_size = 10000
|
175
|
+
file_list: list[FileRecord] = []
|
176
|
+
with connector.session(n_concurrent) as c:
|
177
|
+
file_count = c.count_files(src_path, flat=True)
|
178
|
+
for offset in range(0, file_count, batch_size):
|
179
|
+
file_list.extend(c.list_files(
|
180
|
+
src_path, offset=offset, limit=batch_size, flat=True
|
181
|
+
))
|
182
|
+
|
183
|
+
with BoundedThreadPoolExecutor(n_concurrent) as executor:
|
184
|
+
for file in file_list:
|
185
|
+
executor.submit(get_file, c, file.url)
|
186
|
+
return failed_items
|