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.
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
- tokenInput.value = store.token;
35
- endpointInput.value = store.endpoint;
35
+ // initialization
36
+ store.init();
36
37
  pathInput.value = store.dirpath;
37
38
  uploadFilePrefixLabel.textContent = pathInput.value;
38
- sortBySelect.value = store.sortby;
39
+ sortBySelect.value = store.orderby;
39
40
  sortOrderSelect.value = store.sortorder;
40
- maybeRefreshUserRecord().then(
41
- () => maybeRefreshFileList()
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.put(path, file, {'conflict': 'overwrite'})
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 uploadFile(...args){
181
+ async function uploadFileFn(...args){
176
182
  const [file, path] = args;
177
183
  try{
178
- await conn.put(path, file, {conflict: 'overwrite'});
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(uploadFile(file, path));
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
- /** @param {import('./api.js').DirectoryRecord} dirs */
216
- function sortDirList(dirs){
217
- if (store.sortby === 'name'){
218
- dirs.sort((a, b) => { return a.url.localeCompare(b.url); });
219
- }
220
- if (store.sortorder === 'desc'){ dirs.reverse(); }
221
- }
222
- /** @param {import('./api.js').FileRecord} files */
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 (store.sortby === 'create'){
237
- files.sort((a, b) => { return timestr2num(a.create_time) - timestr2num(b.create_time); });
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 (store.sortby === 'mime'){
240
- files.sort((a, b) => { return a.mime_type.localeCompare(b.mime_type); });
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
- if (store.sortorder === 'desc'){ files.reverse(); }
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
- conn.listPath(store.dirpath)
249
- .then(data => {
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
- console.log("Got data", data);
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 sortby () {
45
- return loadPersistedState('sortby', 'none');
44
+ get orderby () {
45
+ return loadPersistedState('orderby', 'none');
46
46
  },
47
- set sortby (sb) {
48
- setPersistedState('sortby', sb);
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: 10rem;
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(10rem + 1rem);
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
@@ -7,4 +7,10 @@ img.thumb{
7
7
  width: 32px;
8
8
  object-fit: contain;
9
9
  border-radius: 0.1rem;
10
+ transition: all 0.2s;
11
+ }
12
+ img.thumb:hover{
13
+ cursor: pointer;
14
+ height: 48px; /* smaller than backend */
15
+ width: 48px;
10
16
  }
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
  }
@@ -1,6 +1,8 @@
1
1
  import os, time, pathlib
2
2
  from threading import Lock
3
- from .api import Connector
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
- with open(file_path, 'rb') as f:
19
- blob = f.read()
20
- connector.put(dst_url, blob, **put_kwargs)
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
- faild_files = []
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
- connector, file_path, dst_path,
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
- faild_files.append(file_path)
76
+ ))[0]:
77
+ faild_items.append((file_path, res[1]))
70
78
 
71
- with BoundedThreadPoolExecutor(n_concurrent) as executor:
72
- for root, dirs, files in os.walk(directory):
73
- for file in files:
74
- executor.submit(put_file, os.path.join(root, file))
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 faild_files
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
- blob = connector.get(src_url)
95
- if not blob:
96
- return False
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
- with open(file_path, 'wb') as f:
99
- f.write(blob)
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
- failed_files = []
137
- def get_file(src_url):
138
- nonlocal _counter, failed_files
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
- connector, src_url, dst_path,
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
- failed_files.append(src_url)
171
+ ))[0]:
172
+ failed_items.append((src_url, res[1]))
151
173
 
152
- with BoundedThreadPoolExecutor(n_concurrent) as executor:
153
- for file in connector.list_path(src_path, flat=True).files:
154
- executor.submit(get_file, file.url)
155
- return failed_files
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