lfss 0.1.0__py3-none-any.whl → 0.2.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 ADDED
@@ -0,0 +1,419 @@
1
+ import Connector from './api.js';
2
+ import { permMap } from './api.js';
3
+ import { formatSize, decodePathURI, ensurePathURI, copyToClipboard, getRandomString, cvtGMT2Local, debounce } from './utils.js';
4
+
5
+ const conn = new Connector();
6
+ let userRecord = null;
7
+
8
+ const endpointInput = document.querySelector('input#endpoint');
9
+ const tokenInput = document.querySelector('input#token');
10
+ const pathInput = document.querySelector('input#path');
11
+ const pathBackButton = document.querySelector('span#back-btn');
12
+ const pathHintDiv = document.querySelector('#position-hint');
13
+ const pathHintLabel = document.querySelector('#position-hint label');
14
+ const tbody = document.querySelector('#files-table-body');
15
+ const uploadFilePrefixLabel = document.querySelector('#upload-file-prefix');
16
+ const uploadFileSelector = document.querySelector('#file-selector');
17
+ const uploadFileNameInput = document.querySelector('#file-name');
18
+ const uploadButton = document.querySelector('#upload-btn');
19
+ const randomizeFnameButton = document.querySelector('#randomize-fname-btn');
20
+
21
+ conn.config.endpoint = endpointInput.value;
22
+ conn.config.token = tokenInput.value;
23
+
24
+ {
25
+ const endpoint = window.localStorage.getItem('endpoint');
26
+ if (endpoint){
27
+ endpointInput.value = endpoint;
28
+ conn.config.endpoint = endpoint;
29
+ }
30
+ const token = window.localStorage.getItem('token');
31
+ if (token){
32
+ tokenInput.value = token;
33
+ conn.config.token = token;
34
+ }
35
+ const path = window.localStorage.getItem('path');
36
+ if (path){
37
+ pathInput.value = path;
38
+ }
39
+ uploadFilePrefixLabel.textContent = pathInput.value;
40
+ maybeRefreshUserRecord().then(
41
+ () => maybeRefreshFileList()
42
+ );
43
+ }
44
+
45
+ function onPathChange(){
46
+ uploadFilePrefixLabel.textContent = pathInput.value;
47
+ window.localStorage.setItem('path', pathInput.value);
48
+ maybeRefreshFileList();
49
+ }
50
+
51
+ endpointInput.addEventListener('blur', () => {
52
+ conn.config.endpoint = endpointInput.value;
53
+ window.localStorage.setItem('endpoint', endpointInput.value);
54
+ maybeRefreshUserRecord().then(
55
+ () => maybeRefreshFileList()
56
+ );
57
+ });
58
+ tokenInput.addEventListener('blur', () => {
59
+ conn.config.token = tokenInput.value;
60
+ window.localStorage.setItem('token', tokenInput.value);
61
+ maybeRefreshUserRecord().then(
62
+ () => maybeRefreshFileList()
63
+ );
64
+ });
65
+ pathInput.addEventListener('input', () => {
66
+ onPathChange();
67
+ });
68
+ pathBackButton.addEventListener('click', () => {
69
+ const path = pathInput.value;
70
+ if (path.endsWith('/')){
71
+ pathInput.value = path.split('/').slice(0, -2).join('/') + '/';
72
+ }
73
+ else {
74
+ pathInput.value = path.split('/').slice(0, -1).join('/') + '/';
75
+ }
76
+ onPathChange();
77
+ });
78
+
79
+ function onFileNameInpuChange(){
80
+ const fileName = uploadFileNameInput.value;
81
+ if (fileName.length === 0){
82
+ uploadFileNameInput.classList.remove('duplicate');
83
+ }
84
+ else {
85
+ const p = ensurePathURI(pathInput.value + fileName);
86
+ conn.getMetadata(p).then(
87
+ (data) => {
88
+ console.log("Got file meta", data);
89
+ if (data===null) uploadFileNameInput.classList.remove('duplicate');
90
+ else if (data.url) uploadFileNameInput.classList.add('duplicate');
91
+ else throw new Error('Invalid response');
92
+ }
93
+ );
94
+ }
95
+ }
96
+
97
+ randomizeFnameButton.addEventListener('click', () => {
98
+ let currentName = uploadFileNameInput.value;
99
+ let newName = getRandomString(24);
100
+ const dotSplit = currentName.split('.');
101
+ let ext = '';
102
+ if (dotSplit.length > 1){
103
+ ext = dotSplit.pop();
104
+ }
105
+ if (ext.length > 0){
106
+ newName += '.' + ext;
107
+ }
108
+ uploadFileNameInput.value = newName;
109
+ onFileNameInpuChange();
110
+ });
111
+ uploadFileSelector.addEventListener('change', () => {
112
+ uploadFileNameInput.value = uploadFileSelector.files[0].name;
113
+ onFileNameInpuChange();
114
+ });
115
+ uploadButton.addEventListener('click', () => {
116
+ const file = uploadFileSelector.files[0];
117
+ let path = pathInput.value;
118
+ let fileName = uploadFileNameInput.value;
119
+ if (fileName.length === 0){
120
+ throw new Error('File name cannot be empty');
121
+ }
122
+ if (fileName.endsWith('/')){
123
+ throw new Error('File name cannot end with /');
124
+ }
125
+ path = path + fileName;
126
+ conn.put(path, file)
127
+ .then(() => {
128
+ refreshFileList();
129
+ uploadFileNameInput.value = '';
130
+ onFileNameInpuChange();
131
+ }
132
+ );
133
+ });
134
+
135
+ uploadFileNameInput.addEventListener('keydown', (e) => {
136
+ if (e.key === 'Enter'){
137
+ uploadButton.click();
138
+ }
139
+ });
140
+ uploadFileNameInput.addEventListener('input', debounce(onFileNameInpuChange, 500));
141
+
142
+ {
143
+ window.addEventListener('dragover', (e) => {
144
+ e.preventDefault();
145
+ e.stopPropagation();
146
+ });
147
+ window.addEventListener('drop', (e) => {
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ const files = e.dataTransfer.files;
151
+ if (files.length == 1){
152
+ uploadFileSelector.files = files;
153
+ uploadFileNameInput.value = files[0].name;
154
+ uploadFileNameInput.focus();
155
+ }
156
+ else if (files.length > 1){
157
+ let dstPath = pathInput.value + uploadFileNameInput.value;
158
+ if (!dstPath.endsWith('/')){ dstPath += '/'; }
159
+ if (!confirm(`
160
+ You are trying to upload multiple files at once.
161
+ This will directly upload the files to the [${dstPath}] directory without renaming.
162
+ Note that same name files will be overwritten.
163
+ Are you sure you want to proceed?
164
+ `)){ return; }
165
+
166
+ let counter = 0;
167
+ async function uploadFile(...args){
168
+ const [file, path] = args;
169
+ await conn.put(path, file);
170
+ counter += 1;
171
+ console.log("Uploading file: ", counter, "/", files.length);
172
+ }
173
+
174
+ let promises = [];
175
+ for (let i = 0; i < files.length; i++){
176
+ const file = files[i];
177
+ const path = dstPath + file.name;
178
+ promises.push(uploadFile(file, path));
179
+ }
180
+ Promise.all(promises).then(
181
+ () => {
182
+ refreshFileList();
183
+ }
184
+ );
185
+ }
186
+ });
187
+ }
188
+
189
+ function maybeRefreshFileList(){
190
+ if (
191
+ pathInput.value && pathInput.value.length > 0 && pathInput.value.endsWith('/')
192
+ ){
193
+ refreshFileList();
194
+ }
195
+ }
196
+
197
+ function refreshFileList(){
198
+ conn.listPath(pathInput.value)
199
+ .then(data => {
200
+ pathHintDiv.classList.remove('disconnected');
201
+ pathHintDiv.classList.add('connected');
202
+ pathHintLabel.textContent = pathInput.value;
203
+ tbody.innerHTML = '';
204
+
205
+ console.log("Got data", data);
206
+
207
+ if (!data.dirs){ data.dirs = []; }
208
+ if (!data.files){ data.files = []; }
209
+
210
+ data.dirs.forEach(dir => {
211
+ const tr = document.createElement('tr');
212
+ {
213
+ const nameTd = document.createElement('td');
214
+ if (dir.url.endsWith('/')){
215
+ dir.url = dir.url.slice(0, -1);
216
+ }
217
+ const dirName = dir.url.split('/').pop();
218
+ const dirLink = document.createElement('a');
219
+ dirLink.textContent = decodePathURI(dirName);
220
+ dirLink.addEventListener('click', () => {
221
+ let dstUrl = dir.url + (dir.url.endsWith('/') ? '' : '/');
222
+ dstUrl = decodePathURI(dstUrl);
223
+ pathInput.value = dstUrl;
224
+ onPathChange();
225
+ });
226
+ dirLink.href = '#';
227
+ nameTd.appendChild(dirLink);
228
+
229
+ tr.appendChild(nameTd);
230
+ tbody.appendChild(tr);
231
+ }
232
+
233
+ {
234
+ const sizeTd = document.createElement('td');
235
+ sizeTd.textContent = formatSize(dir.size);
236
+ tr.appendChild(sizeTd);
237
+ }
238
+ {
239
+ const dateTd = document.createElement('td');
240
+ tr.appendChild(dateTd);
241
+ }
242
+ {
243
+ const dateTd = document.createElement('td');
244
+ tr.appendChild(dateTd);
245
+ }
246
+ {
247
+ const accessTd = document.createElement('td');
248
+ tr.appendChild(accessTd);
249
+ }
250
+ {
251
+ const actTd = document.createElement('td');
252
+ const actContainer = document.createElement('div');
253
+ actContainer.classList.add('action-container');
254
+
255
+ const downloadButton = document.createElement('a');
256
+ downloadButton.textContent = 'Download';
257
+ downloadButton.href = conn.config.endpoint + '/_api/bundle?path=' + dir.url + (dir.url.endsWith('/') ? '' : '/');
258
+ actContainer.appendChild(downloadButton);
259
+
260
+ const deleteButton = document.createElement('a');
261
+ deleteButton.textContent = 'Delete';
262
+ deleteButton.href = '#';
263
+ deleteButton.addEventListener('click', () => {
264
+ const dirurl = dir.url + (dir.url.endsWith('/') ? '' : '/');
265
+ if (!confirm('[Important] Are you sure you want to delete path ' + dirurl + '?')){
266
+ return;
267
+ }
268
+ conn.delete(dirurl)
269
+ .then(() => {
270
+ refreshFileList();
271
+ });
272
+ });
273
+ actContainer.appendChild(deleteButton);
274
+ actTd.appendChild(actContainer);
275
+ tr.appendChild(actTd);
276
+ }
277
+
278
+ });
279
+ data.files.forEach(file => {
280
+ const tr = document.createElement('tr');
281
+ {
282
+ const nameTd = document.createElement('td');
283
+ const plainUrl = decodePathURI(file.url);
284
+ const fileName = plainUrl.split('/').pop();
285
+ nameTd.textContent = fileName;
286
+ tr.appendChild(nameTd);
287
+ tbody.appendChild(tr);
288
+ }
289
+
290
+ {
291
+ const sizeTd = document.createElement('td');
292
+ const fileSize = file.file_size;
293
+ sizeTd.textContent = formatSize(fileSize);
294
+ tr.appendChild(sizeTd);
295
+ }
296
+
297
+ {
298
+ const dateTd = document.createElement('td');
299
+ const accessTime = file.access_time;
300
+ dateTd.textContent = cvtGMT2Local(accessTime);
301
+ tr.appendChild(dateTd);
302
+ }
303
+
304
+ {
305
+ const dateTd = document.createElement('td');
306
+ const createTime = file.create_time;
307
+ dateTd.textContent = cvtGMT2Local(createTime);
308
+ tr.appendChild(dateTd);
309
+ }
310
+
311
+ {
312
+ const accessTd = document.createElement('td');
313
+ if (file.owner_id === userRecord.id || userRecord.is_admin){
314
+ const select = document.createElement('select');
315
+ select.classList.add('access-select');
316
+ const options = ['unset', 'public', 'protected', 'private'];
317
+ options.forEach(opt => {
318
+ const option = document.createElement('option');
319
+ option.textContent = opt;
320
+ select.appendChild(option);
321
+ });
322
+ select.value = permMap[file.permission];
323
+ select.addEventListener('change', () => {
324
+ const perm = options.indexOf(select.value);
325
+ {
326
+ // ensure the permission is correct!
327
+ const permStr = options[perm];
328
+ const permStrFromMap = permMap[perm];
329
+ if (permStr !== permStrFromMap){
330
+ console.warn("Permission string mismatch", permStr, permStrFromMap);
331
+ }
332
+ }
333
+ conn.setFilePermission(file.url, perm)
334
+ });
335
+
336
+ accessTd.appendChild(select);
337
+ }
338
+ tr.appendChild(accessTd);
339
+ }
340
+
341
+ {
342
+ const actTd = document.createElement('td');
343
+ const actContainer = document.createElement('div');
344
+ actContainer.classList.add('action-container');
345
+
346
+ const copyButton = document.createElement('a');
347
+ copyButton.textContent = 'Copy';
348
+ copyButton.href = '#';
349
+ copyButton.addEventListener('click', () => {
350
+ copyToClipboard(conn.config.endpoint + '/' + file.url);
351
+ });
352
+ actContainer.appendChild(copyButton);
353
+
354
+ const viewButton = document.createElement('a');
355
+ viewButton.textContent = 'View';
356
+ viewButton.href = conn.config.endpoint + '/' + file.url + '?token=' + conn.config.token;
357
+ viewButton.target = '_blank';
358
+ actContainer.appendChild(viewButton);
359
+
360
+ const downloadBtn = document.createElement('a');
361
+ downloadBtn.textContent = 'Download';
362
+ downloadBtn.href = conn.config.endpoint + '/' + file.url + '?asfile=true&token=' + conn.config.token;
363
+ actContainer.appendChild(downloadBtn);
364
+
365
+ const deleteButton = document.createElement('a');
366
+ deleteButton.textContent = 'Delete';
367
+ deleteButton.href = '#';
368
+ deleteButton.addEventListener('click', () => {
369
+ if (!confirm('Are you sure you want to delete ' + file.url + '?')){
370
+ return;
371
+ }
372
+ conn.delete(file.url)
373
+ .then(() => {
374
+ refreshFileList();
375
+ });
376
+ });
377
+ actContainer.appendChild(deleteButton);
378
+
379
+ actTd.appendChild(actContainer);
380
+ tr.appendChild(actTd);
381
+ }
382
+
383
+ });
384
+ },
385
+ (err) => {
386
+ pathHintDiv.classList.remove('connected');
387
+ pathHintDiv.classList.add('disconnected');
388
+ pathHintLabel.textContent = pathInput.value;
389
+ tbody.innerHTML = '';
390
+ console.log("Error");
391
+ console.error(err);
392
+ }
393
+ );
394
+ }
395
+
396
+
397
+ async function maybeRefreshUserRecord(){
398
+ if (endpointInput.value && tokenInput.value){
399
+ await refreshUserRecord();
400
+ }
401
+ }
402
+
403
+ async function refreshUserRecord(){
404
+ try{
405
+ userRecord = await conn.whoami();
406
+ console.log("User record: ", userRecord);
407
+ }
408
+ catch (err){
409
+ userRecord = null;
410
+ console.error("Failed to get user record");
411
+ return false;
412
+ }
413
+
414
+ // UI updates.
415
+
416
+ return true;
417
+ }
418
+
419
+ console.log("Hello World");
frontend/styles.css ADDED
@@ -0,0 +1,211 @@
1
+
2
+ body{
3
+ font-family: Arial, sans-serif;
4
+ background-color: #f1f1f1;
5
+ display: flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ }
9
+
10
+ input[type=button], button{
11
+ background-color: #195f8b;
12
+ color: white;
13
+ padding: 0.8rem;
14
+ margin: 0;
15
+ border: none;
16
+ border-radius: 0.2rem;
17
+ cursor: pointer;
18
+ }
19
+
20
+ input[type=text], input[type=password]
21
+ {
22
+ width: 100%;
23
+ padding: 0.75rem;
24
+ border: 1px solid #ccc;
25
+ border-radius: 0.2rem;
26
+ height: 1rem;
27
+ }
28
+
29
+
30
+ div.container.header, div.container.footer{
31
+ position: fixed;
32
+ left: 0;
33
+ width: 100%;
34
+ background-color: white;
35
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
36
+ display: flex;
37
+ flex-direction: column;
38
+ justify-content: center;
39
+ align-items: center;
40
+ }
41
+
42
+ div.container.header{
43
+ top: 0;
44
+ height: 10rem;
45
+ }
46
+ div.container.footer{
47
+ bottom: 0;
48
+ height: 4rem;
49
+ }
50
+
51
+ div.container.content{
52
+ width: 100%;
53
+ padding-inline: 0.5rem;
54
+ margin-top: calc(10rem + 1rem);
55
+ margin-bottom: calc(4rem + 0.5rem);
56
+ }
57
+
58
+ .input-group{
59
+ display: flex;
60
+ flex-direction: row;
61
+ align-items: center;
62
+ width: calc(100% - 2rem);
63
+ gap: 10px;
64
+ }
65
+ .input-group label{
66
+ min-width: 5rem;
67
+ }
68
+
69
+ span#back-btn{
70
+ position: absolute;
71
+ display: flex;
72
+ justify-content: center;
73
+ align-items: center;
74
+ left: 1.5rem;
75
+ width: 1rem;
76
+ padding: 0.25rem 0.5rem;
77
+ border-radius: 0.5rem;
78
+ background-color: rgb(189, 203, 211);
79
+ cursor: pointer;
80
+ }
81
+
82
+ span#back-btn:hover{
83
+ background-color: rgb(169, 183, 191);
84
+ }
85
+
86
+ span#randomize-fname-btn{
87
+ position: absolute;
88
+ right: calc(6rem + 1rem + 1rem);
89
+ height: 2rem;
90
+ width: 2rem;
91
+ display: flex;
92
+ justify-content: center;
93
+ align-items: center;
94
+ border-radius: 50%;
95
+ cursor: pointer;
96
+ transition: all 0.2s;
97
+ }
98
+ span#randomize-fname-btn:hover{
99
+ background-color: rgb(240, 244, 246);
100
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
101
+ transform: scale(1.1);
102
+ }
103
+ span#randomize-fname-btn:active{
104
+ transform: none;
105
+ }
106
+
107
+ input#path{
108
+ padding-left: 3rem;
109
+ }
110
+
111
+ #upload-btn{
112
+ background-color: #195f8b;
113
+ color: white;
114
+ min-width: 6rem;
115
+ margin: 0;
116
+ border: none;
117
+ border-radius: 0.2rem;
118
+ cursor: pointer;
119
+ }
120
+
121
+ div#position-hint{
122
+ color: rgb(138, 138, 138);
123
+ border-radius: 0.5rem;
124
+ display: flex;
125
+ flex-direction: row;
126
+ align-items: center;
127
+ gap: 0.25rem;
128
+ height: 1rem;
129
+ margin-bottom: 0.25rem;
130
+
131
+ padding: 0.25rem;
132
+ }
133
+ div#position-hint span{
134
+ width: 0.75rem;
135
+ height: 0.75rem;
136
+ border-radius: 50%;
137
+ }
138
+ div#position-hint.disconnected span{
139
+ background-color: red;
140
+ }
141
+ div#position-hint.connected span{
142
+ background-color: rgb(0, 166, 0);
143
+ }
144
+
145
+ div#settings {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 10px;
149
+ }
150
+
151
+ label#bucket-label{
152
+ color: #195f8b;
153
+ }
154
+
155
+ table#files {
156
+ width: 100%;
157
+ border-collapse: collapse;
158
+ }
159
+ table#files th {
160
+ background-color: #195f8b;
161
+ color: white;
162
+ }
163
+ table#files th, table#files td {
164
+ border: 1px solid #ddd;
165
+ padding: 8px;
166
+ text-align: left;
167
+ }
168
+ table#files tr {
169
+ white-space: nowrap;
170
+ }
171
+ table#files tr:hover {
172
+ background-color: #eaeaea;
173
+ transition: all 0.2s;
174
+ }
175
+ table#files tr td:nth-child(2), table#files tr td:nth-child(5){
176
+ width: 1%;
177
+ }
178
+ table#files tr td:nth-child(3), table#files tr td:nth-child(4), table#files tr td:nth-child(6){
179
+ width: 12%;
180
+ }
181
+
182
+ label#upload-file-prefix{
183
+ width: 800px;
184
+ text-align: right;
185
+ }
186
+ input#file-name.duplicate{
187
+ background-color: #ff001511
188
+ }
189
+
190
+ .action-container{
191
+ display: flex;
192
+ flex-direction: row;
193
+ gap: 10px;
194
+ }
195
+ a{
196
+ color: #195f8b;
197
+ text-decoration: none;
198
+ }
199
+ .action-container a{
200
+ padding: 0.2rem;
201
+ padding-inline: 0.5rem;
202
+ border: 1px solid #195f8b;
203
+ border-radius: 0.25rem;
204
+ color: #195f8b;
205
+ transition: all 0.2s;
206
+ }
207
+ .action-container a:hover{
208
+ background-color: #195f8b;
209
+ transform: scale(1.1);
210
+ color: white;
211
+ }
frontend/utils.js ADDED
@@ -0,0 +1,83 @@
1
+
2
+ export function formatSize(size){
3
+ const sizeInKb = size / 1024;
4
+ const sizeInMb = sizeInKb / 1024;
5
+ const sizeInGb = sizeInMb / 1024;
6
+ if (sizeInGb > 1){
7
+ return sizeInGb.toFixed(2) + ' GB';
8
+ }
9
+ else if (sizeInMb > 1){
10
+ return sizeInMb.toFixed(2) + ' MB';
11
+ }
12
+ else if (sizeInKb > 1){
13
+ return sizeInKb.toFixed(2) + ' KB';
14
+ }
15
+ else {
16
+ return size + ' B';
17
+ }
18
+ }
19
+
20
+ export function copyToClipboard(text){
21
+ function secureCopy(text){
22
+ navigator.clipboard.writeText(text);
23
+ }
24
+ function unsecureCopy(text){
25
+ const el = document.createElement('textarea');
26
+ el.value = text;
27
+ document.body.appendChild(el);
28
+ el.select();
29
+ document.execCommand('copy');
30
+ document.body.removeChild(el);
31
+ }
32
+ if (navigator.clipboard){
33
+ secureCopy(text);
34
+ }
35
+ else {
36
+ unsecureCopy(text);
37
+ }
38
+ }
39
+
40
+ export function encodePathURI(path){
41
+ return path.split('/').map(encodeURIComponent).join('/');
42
+ }
43
+
44
+ export function decodePathURI(path){
45
+ return path.split('/').map(decodeURIComponent).join('/');
46
+ }
47
+
48
+ export function ensurePathURI(path){
49
+ return encodePathURI(decodePathURI(path));
50
+ }
51
+
52
+ export function getRandomString(n, additionalCharset='0123456789_-(=)[]{}'){
53
+ let result = '';
54
+ let charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
55
+ const firstChar = charset[Math.floor(Math.random() * charset.length)];
56
+ const lastChar = charset[Math.floor(Math.random() * charset.length)];
57
+ result += firstChar;
58
+ charset += additionalCharset;
59
+ for (let i = 0; i < n-2; i++){
60
+ result += charset[Math.floor(Math.random() * charset.length)];
61
+ }
62
+ result += lastChar;
63
+ return result;
64
+ };
65
+
66
+ /**
67
+ * @param {string} dateStr
68
+ * @returns {string}
69
+ */
70
+ export function cvtGMT2Local(dateStr){
71
+ const gmtdate = new Date(dateStr);
72
+ const localdate = new Date(gmtdate.getTime() + gmtdate.getTimezoneOffset() * 60000);
73
+ return localdate.toISOString().slice(0, 19).replace('T', ' ');
74
+ }
75
+
76
+ export function debounce(fn,wait){
77
+ let timeout;
78
+ return function(...args){
79
+ const context = this;
80
+ if (timeout) clearTimeout(timeout);
81
+ timeout = setTimeout(() => fn.apply(context, args), wait);
82
+ }
83
+ }