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.
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
- 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
  });
@@ -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,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
- faild_files = []
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
- connector, file_path, dst_path,
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
- faild_files.append(file_path)
72
+ ))[0]:
73
+ faild_items.append((file_path, res[1]))
70
74
 
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))
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 faild_files
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 not blob:
96
- return False
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
- failed_files = []
137
- def get_file(src_url):
138
- nonlocal _counter, failed_files
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
- connector, src_url, dst_path,
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
- failed_files.append(src_url)
157
+ ))[0]:
158
+ failed_items.append((src_url, res[1]))
151
159
 
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
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