hfs 0.42.3 → 0.43.0
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.
- package/README.md +6 -8
- package/admin/assets/{index-08017e15.js → index-5cd667a5.js} +134 -134
- package/admin/assets/index-8ff39373.css +1 -0
- package/{frontend/assets/sha512-f6798733.js → admin/assets/sha512-55ff2fa3.js} +1 -1
- package/admin/index.html +2 -2
- package/frontend/assets/index-27488fde.js +94 -0
- package/frontend/assets/index-54a5c76f.css +1 -0
- package/{admin/assets/sha512-69b26793.js → frontend/assets/sha512-8ebf6e2a.js} +1 -1
- package/frontend/fontello.css +9 -3
- package/frontend/fontello.woff2 +0 -0
- package/frontend/index.html +2 -2
- package/package.json +1 -1
- package/plugins/antibrute/plugin.js +1 -1
- package/plugins/download-counter/plugin.js +10 -3
- package/plugins/download-counter/public/main.js +12 -2
- package/src/adminApis.js +3 -3
- package/src/api.file_list.js +15 -2
- package/src/api.lang.js +8 -11
- package/src/api.vfs.js +2 -1
- package/src/block.js +6 -20
- package/src/config.js +6 -2
- package/src/const.js +2 -2
- package/src/customHtml.js +1 -1
- package/src/frontEndApis.js +1 -26
- package/src/lang.js +77 -0
- package/src/langs/hfs-lang-it.json +100 -0
- package/src/langs/hfs-lang-ko.json +103 -0
- package/src/langs/hfs-lang-ru.json +106 -0
- package/src/langs/hfs-lang-sr.json +108 -0
- package/src/langs/hfs-lang-zh.json +98 -0
- package/src/listen.js +8 -3
- package/src/middlewares.js +10 -0
- package/src/misc.js +13 -9
- package/src/perm.js +1 -1
- package/src/serveFile.js +2 -2
- package/src/serveGuiFiles.js +21 -8
- package/src/upload.js +1 -1
- package/src/vfs.js +19 -27
- package/admin/assets/index-94bbe0be.css +0 -1
- package/frontend/assets/index-5f125477.js +0 -94
- package/frontend/assets/index-a09cacfd.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@charset "UTF-8";:root{height:100dvh;--bg: #fff;--text: #555;--ghost-contrast: #8882;--faint-contrast: #8884;--mild-contrast: #8886;--good-contrast: #000a;--button-bg: #68a;--button-text: #eaeaea;--focus-color: #468;--separator: " \2013 "}:root .highlightedText,:root .file-menu a:hover{color:#0006;text-shadow:0 0 3px rgba(0,0,0,.4)}:root .theme-dark{--bg: #000;--text: #999;--good-contrast: #fffa;--button-bg: #345;--button-text: #999;color-scheme:dark}:root .theme-dark .highlightedText,:root .theme-dark .file-menu a:hover,:root .file-menu .theme-dark a:hover{color:#fff;text-shadow:0 0 3px #fff}:root .theme-dark a{color:#8ac}:root .theme-dark .dialog-closer{background:#633}:root .theme-dark .dialog-icon{color:#ccc}:root .theme-dark .dialog-icon .icon{color:#aaa;margin-left:-1px;font-size:95%}:root .theme-dark .dialog-backdrop{background:rgba(51,51,51,.7333333333)}:root .theme-dark .error-msg{color:#b88;background-color:#623}:root .theme-dark button.toggled{color:#eee}body{background:var(--bg);margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body,button,select,input{font-size:12pt}#root{max-width:50em;margin:auto;min-height:100vh;display:flex;flex-direction:column}body,input{color:var(--text)}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}input:not([type=checkbox]),select{padding:.3em .4em;border-radius:.5em;background:var(--bg);border-color:var(--mild-contrast);color:var(--good-contrast);max-width:100%;box-sizing:border-box;width:100%}input[type=checkbox]{transform:scale(1.7);accent-color:var(--button-bg)}label input[type=checkbox]{margin-right:.8em}select{text-align:center}.hidden{display:none!important}.icon{font-size:1.2em}.emoji-icon,.file-icon{display:inline-block;width:1.4em;text-align:center}.file-icon{height:1em;background-size:contain;background-repeat:no-repeat;background-position:center;vertical-align:text-bottom}.icon.mirror:before{transform:scaleX(-1)}a{text-decoration:none;color:#57a}button{background-color:var(--button-bg);color:var(--button-text);padding:.5em 1em;border:transparent;text-decoration:none;border-radius:.3em;vertical-align:middle;cursor:pointer}button:hover{outline:1px solid var(--mild-contrast)}button.toggled{color:#fff;text-shadow:0 0 3px #fff}button:focus-visible,.breadcrumb:focus-visible{outline:3px solid var(--focus-color)}a>button{width:100%}input:focus-visible,select:focus-visible,ul a:focus-visible{border-radius:.3em;border-color:transparent;outline:2px solid var(--focus-color)}.icon-button,ul.dir li .entry-panel .file-menu-button{font-size:.7em;padding:.2em .4em;margin-left:.4em;vertical-align:bottom}.error-msg{background-color:#faa;color:#833;padding:.5em 1em}header{position:sticky;top:0;background:var(--bg);padding:.2em;z-index:1}.before-sliding{width:0!important;flex:0!important;margin:0!important;height:0!important;padding:0!important;overflow:hidden!important;transition:flex .5s}.show-sliding{transition:flex .5s;overflow:clip;flex:1;white-space:nowrap}.ani-working{animation:1s blink infinite}@keyframes blink{0%{opacity:1}50%{opacity:.2}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.spinner,.icon.spinner:before{animation:1.5s spin infinite linear;display:inline-flex;justify-content:center;align-items:center;width:min-content}.icon.emoji.spinner{display:inline-block}.breadcrumb{padding:.1em .6em .2em;line-height:1.8em;border-radius:.7em;background-color:var(--button-bg);color:var(--button-text);border-top:1px solid #666;margin-right:-.1em}.breadcrumb:nth-child(-n+3) .icon{padding:0 .2em}#folder-stats,#filter-bar>span{font-size:90%}#folder-stats{margin:.5em 0 0 .5em;float:right}#folder-stats .icon{margin-right:.3em}#filter{flex:1;box-sizing:border-box}#filter-bar{display:flex;align-items:center;gap:.8em;margin:.2em 0 0;padding:2px 0 1px 11px;height:1.8em}#filter-bar input[type=checkbox]{margin-top:.5em}#filter-bar span:empty{display:none}ul.dir{flex:1;padding:0;margin:0;clear:both}ul.dir>p{text-align:center}ul.dir li{display:block;list-style-type:none;padding:.3em .3em .4em;border-bottom:1px solid var(--faint-contrast)}ul.dir li:nth-of-type(odd){background-color:var(--ghost-contrast)}ul.dir li input[type=checkbox]{margin:0 .8em}ul.dir li a:last-of-type{word-break:break-word;padding-right:.3em}ul.dir li a .icon{margin-right:.3em}ul.dir li a:hover{text-decoration:underline}ul.dir li .entry-panel{float:right;margin-top:.3em;display:flex;align-items:center}ul.dir li .entry-panel .file-menu-button{margin:-3px 0 -3px .4em}ul.dir li .entry-panel .entry-props{font-size:90%;margin-left:4px;font-variant-numeric:tabular-nums}ul.dir li .entry-panel .entry-props .entry-size-unit{margin-left:.3em}ul.dir li>div:last-of-type{clear:both}ul.dir li.page-separator{margin-top:1em;position:relative}ul.dir li.page-separator:before{content:attr(label);position:absolute;top:-1.8em;font-size:smaller;margin-left:calc(50% - 1em);opacity:.9}#menu-bar{display:flex;justify-content:space-evenly;flex-wrap:wrap}#menu-bar>*{flex:1;margin:.1em}#menu-bar button{padding-left:0;padding-right:0}#searched{margin:.2em}#user-panel{display:flex;flex-direction:column;gap:1em}#user-panel a>button{width:100%}button label{cursor:inherit;margin-left:.5em}.dialog-backdrop.working{font-size:5em;animation:1s fade-in}.dialog-content{padding:.2em}.dialog-alert .dialog-content{text-align:center}.dialog-alert .dialog-content p{text-align:left;display:inline-block}.dialog{min-width:11em;--color: var(--button-bg)}#paging{position:sticky;bottom:0;display:flex;gap:.1em;background:var(--bg);padding:0 .2em .2em}#paging>button{z-index:1}#paging button{box-shadow:0 0 .3em .3em #0003}#paging #paging-middle{padding:0 .5em;margin:0 -.3em;display:flex;gap:.5em;flex:1;overflow-x:auto}#paging #paging-middle>button{flex:1;padding-top:0;padding-bottom:0}#paging button{background:var(--button-bg);text-align:center;white-space:nowrap;padding:.5em}.upload-progress:before{content:var(--separator)}.entry-size:after{content:var(--separator)}.upload-progress{min-width:4em;display:inline-block;margin-left:.5em}.upload-list td:nth-child(1){width:0}.upload-list td:nth-child(2){text-align:right;width:0;white-space:nowrap;padding-left:.5em}.upload-list td:nth-child(3){padding:.2em .5em;word-break:break-word}.dialog-login form{display:flex;flex-direction:column;gap:1.2em}.dialog-login label{display:block;margin-bottom:.5em;margin-left:.1em}.miss-perm{margin:.3em}.popup-menu-button{font-size:.8em;padding:.2em .3em;position:absolute;opacity:.8}.popup-menu-button:hover{opacity:1}.file-dialog .dialog-content{min-width:calc(100% - 1em)}.file-dialog .dialog{min-width:13em}.file-dialog-properties{word-break:break-word;line-height:1.5em}.file-dialog-properties dt{font-weight:700}.file-dialog-properties dd{margin-left:1.5em}.file-menu{margin-top:1em;display:flex;flex-direction:column}.file-menu a{padding:.5em 0}.file-menu a:first-child{padding-top:1em;border-top:1px solid var(--faint-contrast)}.file-menu a .icon{margin-right:.5em}@media (min-width: 42em){body{scrollbar-width:thin;scrollbar-color:var(--button-bg) var(--ghost-contrast)}body::-webkit-scrollbar{width:12px}body::-webkit-scrollbar-track{background:var(--ghost-contrast)}body::-webkit-scrollbar-thumb{background-color:var(--button-bg);border-radius:20px;border:1px solid var(--ghost-contrast)}}@media (max-width: 42em){:root{--ghost-contrast: #8883}body,button,select{font-size:14pt}#menu-bar button label,#filter-bar button label{display:none}#filter-bar{margin-top:.4em}#filter-bar button{width:17.6vw;height:2.3em}.breadcrumb{word-break:break-all}.breadcrumb .icon{font-size:24px}}.dialog-backdrop{position:fixed;inset:0;background:#888a;display:flex;justify-content:center;align-items:center;z-index:1000}.dialog{background:#fff;background:var(--bg);padding:max(.5em,1vw);border-radius:1em;position:relative;margin:0 3vw;overflow:hidden;max-height:calc(100vh - 2em);display:flex;flex-direction:column;justify-content:center}.dialog-icon{color:#fff;background-color:var(--color);position:absolute;top:0;width:2em;height:1.8em;text-align:center;border-radius:.8em 0}.dialog-title{font-size:120%;margin-top:-.4em;padding:0 .5em}.dialog-closer~.dialog-title{margin-right:2em}.dialog-type~.dialog-title{margin-left:2em}.dialog-icon~.dialog-title{text-align:center}.dialog-closer{border-radius:0 .8em;right:0;padding:0;background-color:#c88}.dialog-icon~.dialog-content{margin-top:2em}.dialog-type{left:0;top:0;overflow:hidden;line-height:1.8em;opacity:.8}.dialog-content{overflow:auto;max-height:calc(100vh - 4.5em)}.dialog-content p{white-space:pre-wrap;margin:.5em 0}.dialog-confirm .dialog-content button{margin-top:1em}.dialog-alert-info{--color: #282 }.dialog-alert-warning{--color: #c91 }.dialog-alert-error{--color: #822}@media (max-width: 42em){.dialog-icon{font-size:120%}.dialog-icon~.dialog-content{margin-top:2.5em}.dialog-title{margin-top:-.2em}}.dialog-prompt label{display:block;margin-bottom:.5em;margin-left:.1em}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as SF}from"./index-
|
|
1
|
+
import{c as SF}from"./index-27488fde.js";function OF(iF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in iF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(iF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(iF,Symbol.toStringTag,{value:"Module"}))}var EF={},UF={get exports(){return EF},set exports(iF){EF=iF}};/*
|
|
2
2
|
* [js-sha512]{@link https://github.com/emn178/js-sha512}
|
|
3
3
|
*
|
|
4
4
|
* @version 0.8.0
|
package/frontend/fontello.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@font-face {
|
|
2
2
|
font-family: 'fontello';
|
|
3
|
-
src: url('fontello.woff2?
|
|
3
|
+
src: url('fontello.woff2?19258766') format('woff2'); font-weight: normal;
|
|
4
|
+
font-weight: normal;
|
|
4
5
|
font-style: normal;
|
|
5
6
|
}
|
|
6
7
|
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
|
10
11
|
@font-face {
|
|
11
12
|
font-family: 'fontello';
|
|
12
|
-
src: url('../font/fontello.svg?
|
|
13
|
+
src: url('../font/fontello.svg?19258766#fontello') format('svg');
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
*/
|
|
@@ -49,9 +50,11 @@
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
.fa-cog:before { content: '\e800'; } /* '' */
|
|
53
|
+
.fa-audio:before { content: '\e801'; } /* '' */
|
|
52
54
|
.fa-doc:before { content: '\e802'; } /* '' */
|
|
53
55
|
.fa-stop:before { content: '\e803'; } /* '' */
|
|
54
56
|
.fa-play:before { content: '\e804'; } /* '' */
|
|
57
|
+
.fa-music:before { content: '\e805'; } /* '' */
|
|
55
58
|
.fa-cancel:before { content: '\e806'; } /* '' */
|
|
56
59
|
.fa-edit:before { content: '\e807'; } /* '' */
|
|
57
60
|
.fa-check:before { content: '\e808'; } /* '' */
|
|
@@ -63,8 +66,12 @@
|
|
|
63
66
|
.fa-to_start:before { content: '\e80e'; } /* '' */
|
|
64
67
|
.fa-retweet:before { content: '\e80f'; } /* '' */
|
|
65
68
|
.fa-to_end:before { content: '\e810'; } /* '' */
|
|
69
|
+
.fa-picture:before { content: '\e811'; } /* '' */
|
|
70
|
+
.fa-camera:before { content: '\e812'; } /* '' */
|
|
66
71
|
.fa-search:before { content: '\e813'; } /* '' */
|
|
67
72
|
.fa-logout:before { content: '\e814'; } /* '' */
|
|
73
|
+
.fa-video:before { content: '\e815'; } /* '' */
|
|
74
|
+
.fa-left:before { content: '\e816'; } /* '' */
|
|
68
75
|
.fa-spin6:before { content: '\e839'; } /* '' */
|
|
69
76
|
.fa-crown:before { content: '\e844'; } /* '' */
|
|
70
77
|
.fa-download:before { content: '\f02e'; } /* '' */
|
|
@@ -73,5 +80,4 @@
|
|
|
73
80
|
.fa-menu:before { content: '\f0c9'; } /* '' */
|
|
74
81
|
.fa-quote:before { content: '\f10d'; } /* '' */
|
|
75
82
|
.fa-unlink:before { content: '\f127'; } /* '' */
|
|
76
|
-
.fa-level-up:before { content: '\f148'; } /* '' */
|
|
77
83
|
.fa-archive:before { content: '\f1c6'; } /* '' */
|
package/frontend/fontello.woff2
CHANGED
|
Binary file
|
package/frontend/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
|
|
6
6
|
<link href="/fontello.css" rel="stylesheet" />
|
|
7
7
|
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-27488fde.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="/assets/index-54a5c76f.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
package/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
exports.description = "Counts downloads for each file, and displays the total in the list"
|
|
2
|
-
exports.version =
|
|
3
|
-
exports.apiRequired =
|
|
1
|
+
exports.description = "Counts downloads for each file, and displays the total in the list or file menu"
|
|
2
|
+
exports.version = 4 // config.where
|
|
3
|
+
exports.apiRequired = 8
|
|
4
|
+
|
|
5
|
+
exports.config = {
|
|
6
|
+
where: { frontend: true, type: 'select', options: ['list', 'menu'] }
|
|
7
|
+
}
|
|
8
|
+
exports.configDialog = {
|
|
9
|
+
sx: { maxWidth: '20em' },
|
|
10
|
+
}
|
|
4
11
|
|
|
5
12
|
exports.init = async api => {
|
|
6
13
|
const _ = api.require('lodash')
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
(() => { // this wrapper avoids name clashing of outer variables and functions
|
|
2
|
+
const config = HFS.getPluginConfig()
|
|
3
|
+
|
|
4
|
+
const inMenu = config.where === 'menu'
|
|
5
|
+
HFS.onEvent('additionalEntryProps', ({ entry: { hits } }, { t }) =>
|
|
6
|
+
hits && !inMenu && `<span class="download-counter" title="${t`download counter`}">${hits}</span>`)
|
|
7
|
+
|
|
8
|
+
HFS.onEvent('fileMenu', ({ entry, props }) => {
|
|
9
|
+
if (inMenu && !entry.isFolder)
|
|
10
|
+
props.push(["Downloads", entry.hits || 0])
|
|
11
|
+
})
|
|
12
|
+
})()
|
package/src/adminApis.js
CHANGED
|
@@ -167,8 +167,8 @@ for (const [k, was] of Object.entries(exports.adminApis))
|
|
|
167
167
|
: new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, props);
|
|
168
168
|
};
|
|
169
169
|
exports.localhostAdmin = (0, config_1.defineConfig)('localhost_admin', true);
|
|
170
|
-
exports.adminNet = (0, config_1.defineConfig)('admin_net', '');
|
|
171
|
-
exports.favicon = (0, config_1.defineConfig)('favicon');
|
|
170
|
+
exports.adminNet = (0, config_1.defineConfig)('admin_net', '', v => (0, misc_1.makeNetMatcher)(v, true));
|
|
171
|
+
exports.favicon = (0, config_1.defineConfig)('favicon', '');
|
|
172
172
|
exports.title = (0, config_1.defineConfig)('title', "File server");
|
|
173
173
|
function ctxAdminAccess(ctx) {
|
|
174
174
|
return !ctx.state.proxiedFor // we consider localhost_admin only if no proxy is detected
|
|
@@ -187,6 +187,6 @@ function anyAccountCanLoginAdmin() {
|
|
|
187
187
|
}
|
|
188
188
|
exports.anyAccountCanLoginAdmin = anyAccountCanLoginAdmin;
|
|
189
189
|
function allowAdmin(ctx) {
|
|
190
|
-
return
|
|
190
|
+
return exports.adminNet.compiled()(ctx.ip);
|
|
191
191
|
}
|
|
192
192
|
exports.allowAdmin = allowAdmin;
|
package/src/api.file_list.js
CHANGED
|
@@ -58,7 +58,7 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
|
|
|
58
58
|
break;
|
|
59
59
|
if (!filter((0, vfs_1.getNodeName)(sub)))
|
|
60
60
|
continue;
|
|
61
|
-
const entry = await nodeToDirEntry(sub);
|
|
61
|
+
const entry = await nodeToDirEntry(ctx, sub);
|
|
62
62
|
if (!entry)
|
|
63
63
|
continue;
|
|
64
64
|
const cbParams = { entry, ctx, listPath: path, node: sub };
|
|
@@ -87,7 +87,7 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
|
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
89
|
exports.file_list = file_list;
|
|
90
|
-
async function nodeToDirEntry(node) {
|
|
90
|
+
async function nodeToDirEntry(ctx, node) {
|
|
91
91
|
let { source, default: def } = node;
|
|
92
92
|
const name = (0, vfs_1.getNodeName)(node);
|
|
93
93
|
if (!source)
|
|
@@ -98,14 +98,27 @@ async function nodeToDirEntry(node) {
|
|
|
98
98
|
const st = await (0, promises_1.stat)(source);
|
|
99
99
|
const folder = st.isDirectory();
|
|
100
100
|
const { ctime, mtime } = st;
|
|
101
|
+
const pl = node.can_list === vfs_1.WHO_NO_ONE ? 'l'
|
|
102
|
+
: !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
|
|
103
|
+
: '';
|
|
104
|
+
// no download here, but maybe inside?
|
|
105
|
+
const pr = node.can_read === vfs_1.WHO_NO_ONE && !(folder && filesInsideCould()) ? 'r'
|
|
106
|
+
: !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
|
|
107
|
+
: '';
|
|
101
108
|
return {
|
|
102
109
|
n: name + (folder ? '/' : ''),
|
|
103
110
|
c: ctime,
|
|
104
111
|
m: Math.abs(+mtime - +ctime) < 1000 ? undefined : mtime,
|
|
105
112
|
s: folder ? undefined : st.size,
|
|
113
|
+
p: (pr + pl) || undefined
|
|
106
114
|
};
|
|
107
115
|
}
|
|
108
116
|
catch (_a) {
|
|
109
117
|
return null;
|
|
110
118
|
}
|
|
119
|
+
function filesInsideCould(n = node) {
|
|
120
|
+
var _a;
|
|
121
|
+
return (0, vfs_1.masksCouldGivePermission)(n.masks, 'can_read')
|
|
122
|
+
|| ((_a = n.children) === null || _a === void 0 ? void 0 : _a.some(c => c.can_read || filesInsideCould(c))); // we count on the boolean-compliant nature of the permission type here
|
|
123
|
+
}
|
|
111
124
|
}
|
package/src/api.lang.js
CHANGED
|
@@ -10,21 +10,22 @@ const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
|
10
10
|
const promises_1 = require("fs/promises");
|
|
11
11
|
const const_1 = require("./const");
|
|
12
12
|
const misc_1 = require("./misc");
|
|
13
|
-
const
|
|
14
|
-
const SUFFIX = '.json';
|
|
13
|
+
const lang_1 = require("./lang");
|
|
15
14
|
const apis = {
|
|
16
15
|
list_langs() {
|
|
17
16
|
return new apiMiddleware_1.SendListReadable({
|
|
18
17
|
doAtStart: async (list) => {
|
|
19
|
-
for await (let name of fast_glob_1.default.stream(code2file('*'))) {
|
|
18
|
+
for await (let name of fast_glob_1.default.stream((0, lang_1.code2file)('*'))) {
|
|
20
19
|
name = String(name);
|
|
21
|
-
const code =
|
|
20
|
+
const code = (0, lang_1.file2code)(name);
|
|
22
21
|
try {
|
|
23
22
|
const data = JSON.parse(await (0, promises_1.readFile)(name, 'utf8'));
|
|
24
23
|
list.add({ code, ...lodash_1.default.omit(data, 'translate') });
|
|
25
24
|
}
|
|
26
25
|
catch (_a) { }
|
|
27
26
|
}
|
|
27
|
+
for (const [code, data] of Object.entries(lang_1.EMBEDDED_TRANSLATIONS))
|
|
28
|
+
list.add({ code, embedded: true, ...lodash_1.default.omit(data, 'translate') });
|
|
28
29
|
list.close();
|
|
29
30
|
}
|
|
30
31
|
});
|
|
@@ -32,7 +33,7 @@ const apis = {
|
|
|
32
33
|
async del_lang({ code }) {
|
|
33
34
|
validateCode(code);
|
|
34
35
|
try {
|
|
35
|
-
await (0, promises_1.rm)(code2file(code));
|
|
36
|
+
await (0, promises_1.rm)((0, lang_1.code2file)(code));
|
|
36
37
|
return {};
|
|
37
38
|
}
|
|
38
39
|
catch (e) {
|
|
@@ -41,10 +42,9 @@ const apis = {
|
|
|
41
42
|
},
|
|
42
43
|
async add_langs({ langs }) {
|
|
43
44
|
for (let [code, content] of Object.entries(langs)) {
|
|
44
|
-
|
|
45
|
-
code = code.slice(PREFIX.length, -SUFFIX.length);
|
|
45
|
+
code = (0, lang_1.file2code)(code);
|
|
46
46
|
validateCode(code);
|
|
47
|
-
const fn = code2file(code);
|
|
47
|
+
const fn = (0, lang_1.code2file)(code);
|
|
48
48
|
const s = content = String(content);
|
|
49
49
|
if (!(0, misc_1.tryJson)(s))
|
|
50
50
|
return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, "bad content for file " + fn);
|
|
@@ -54,9 +54,6 @@ const apis = {
|
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
exports.default = apis;
|
|
57
|
-
function code2file(code) {
|
|
58
|
-
return PREFIX + code.toLowerCase() + SUFFIX;
|
|
59
|
-
}
|
|
60
57
|
function validateCode(code) {
|
|
61
58
|
if (!/^(\w\w)(-\w\w)*$/.test(code))
|
|
62
59
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad code/filename');
|
package/src/api.vfs.js
CHANGED
|
@@ -154,13 +154,14 @@ const apis = {
|
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
try {
|
|
157
|
+
const matching = (0, misc_1.makeMatcher)(fileMask);
|
|
157
158
|
path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
|
|
158
159
|
for await (const [name, isDir] of (0, misc_1.dirStream)(path)) {
|
|
159
160
|
if (ctx.req.aborted)
|
|
160
161
|
return;
|
|
161
162
|
try {
|
|
162
163
|
if (!isDir)
|
|
163
|
-
if (!files || fileMask && !(
|
|
164
|
+
if (!files || fileMask && !matching(name))
|
|
164
165
|
continue;
|
|
165
166
|
const stats = await (0, promises_1.stat)((0, path_1.join)(path, name));
|
|
166
167
|
yield {
|
package/src/block.js
CHANGED
|
@@ -1,34 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
4
|
exports.applyBlock = void 0;
|
|
8
5
|
const config_1 = require("./config");
|
|
9
6
|
const connections_1 = require("./connections");
|
|
10
7
|
const misc_1 = require("./misc");
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
compileBlock(rules);
|
|
8
|
+
const block = (0, config_1.defineConfig)('block', [], rules => {
|
|
9
|
+
const ret = !Array.isArray(rules) ? []
|
|
10
|
+
: (0, misc_1.onlyTruthy)(rules.map(rule => (0, misc_1.makeNetMatcher)(rule.ip, true)));
|
|
11
|
+
// reapply new block to existing connections
|
|
16
12
|
for (const { socket, ip } of (0, connections_1.getConnections)())
|
|
17
13
|
applyBlock(socket, ip);
|
|
14
|
+
return ret;
|
|
18
15
|
});
|
|
19
|
-
function compileBlock(rules) {
|
|
20
|
-
blockFunctions = !Array.isArray(rules) ? []
|
|
21
|
-
: (0, misc_1.onlyTruthy)(rules.map(rule => !rule ? null
|
|
22
|
-
: (0, misc_1.with_)(rule.ip, ip => typeof ip !== 'string' ? null
|
|
23
|
-
: ip.includes('/') ? x => cidr_tools_1.default.contains(ip, x)
|
|
24
|
-
: ip.includes('*') ? (0, misc_1.with_)(ipMask2regExp(ip), re => x => re.test(x))
|
|
25
|
-
: x => x === ip)));
|
|
26
|
-
function ipMask2regExp(ipMask) {
|
|
27
|
-
return new RegExp(lodash_1.default.escapeRegExp(ipMask).replace(/\\\*/g, '.*'));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
16
|
function applyBlock(socket, ip = (0, connections_1.normalizeIp)(socket.remoteAddress || '')) {
|
|
31
|
-
if (ip &&
|
|
17
|
+
if (ip && block.compiled().find(rule => rule(ip)))
|
|
32
18
|
return socket.destroy();
|
|
33
19
|
}
|
|
34
20
|
exports.applyBlock = applyBlock;
|
package/src/config.js
CHANGED
|
@@ -51,8 +51,11 @@ const { save } = (0, watchLoad_1.watchLoad)(path, values => setConfig(values ||
|
|
|
51
51
|
setConfig({}, false);
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
-
function defineConfig(k, defaultValue) {
|
|
54
|
+
function defineConfig(k, defaultValue, compiler = lodash_1.default.identity) {
|
|
55
55
|
configProps[k] = { defaultValue };
|
|
56
|
+
let compiled = compiler(defaultValue);
|
|
57
|
+
if (compiler)
|
|
58
|
+
subscribeConfig(k, (v) => compiled = compiler(v));
|
|
56
59
|
return {
|
|
57
60
|
key() {
|
|
58
61
|
return k;
|
|
@@ -68,7 +71,8 @@ function defineConfig(k, defaultValue) {
|
|
|
68
71
|
this.set(v(this.get()));
|
|
69
72
|
else
|
|
70
73
|
setConfig1(k, v);
|
|
71
|
-
}
|
|
74
|
+
},
|
|
75
|
+
compiled: () => compiled
|
|
72
76
|
};
|
|
73
77
|
}
|
|
74
78
|
exports.defineConfig = defineConfig;
|
package/src/const.js
CHANGED
|
@@ -38,12 +38,12 @@ exports.DEV = process.env.DEV || exports.argv.dev ? 'DEV' : '';
|
|
|
38
38
|
exports.ORIGINAL_CWD = process.cwd();
|
|
39
39
|
exports.HFS_STARTED = new Date();
|
|
40
40
|
const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
|
|
41
|
-
exports.BUILD_TIMESTAMP = "2023-
|
|
41
|
+
exports.BUILD_TIMESTAMP = "2023-04-08T13:40:53.294Z";
|
|
42
42
|
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
|
|
43
43
|
exports.VERSION = pkg.version;
|
|
44
44
|
exports.DAY = 86400000;
|
|
45
45
|
exports.SESSION_DURATION = exports.DAY;
|
|
46
|
-
exports.API_VERSION =
|
|
46
|
+
exports.API_VERSION = 8; // entry.uri + script.plugin
|
|
47
47
|
exports.COMPATIBLE_API_VERSION = 1; // while changes in the api are not breaking, this number stays the same, otherwise is made equal to API_VERSION
|
|
48
48
|
exports.SPECIAL_URI = '/~/';
|
|
49
49
|
exports.FRONTEND_URI = exports.SPECIAL_URI + 'frontend/';
|
package/src/customHtml.js
CHANGED
|
@@ -21,7 +21,7 @@ if (!(0, fs_1.existsSync)(FILE))
|
|
|
21
21
|
events_1.default.once('config ready', () => {
|
|
22
22
|
const legacy = (0, misc_1.prefix)('[beforeHeader]\n', frontEndApis_1.customHeader.get());
|
|
23
23
|
(0, fs_1.writeFileSync)(FILE, legacy);
|
|
24
|
-
frontEndApis_1.customHeader.set(
|
|
24
|
+
frontEndApis_1.customHeader.set(''); // get rid of it
|
|
25
25
|
});
|
|
26
26
|
(0, watchLoad_1.watchLoad)(FILE, data => {
|
|
27
27
|
var _a;
|
package/src/frontEndApis.js
CHANGED
|
@@ -38,8 +38,7 @@ const const_1 = require("./const");
|
|
|
38
38
|
const vfs_1 = require("./vfs");
|
|
39
39
|
const promises_1 = require("fs/promises");
|
|
40
40
|
const path_1 = require("path");
|
|
41
|
-
|
|
42
|
-
exports.customHeader = (0, config_1.defineConfig)('custom_header');
|
|
41
|
+
exports.customHeader = (0, config_1.defineConfig)('custom_header', '');
|
|
43
42
|
exports.frontEndApis = {
|
|
44
43
|
file_list: api_file_list_1.file_list,
|
|
45
44
|
...api_auth,
|
|
@@ -87,30 +86,6 @@ exports.frontEndApis = {
|
|
|
87
86
|
throw new apiMiddleware_1.ApiError(e.code || const_1.HTTP_SERVER_ERROR, e);
|
|
88
87
|
}
|
|
89
88
|
},
|
|
90
|
-
async load_lang({ lang, embedded }) {
|
|
91
|
-
const ret = {};
|
|
92
|
-
const langs = (0, misc_1.wantArray)(lang).map(x => x.toLowerCase());
|
|
93
|
-
let i = 0;
|
|
94
|
-
while (i < langs.length) {
|
|
95
|
-
let x = langs[i];
|
|
96
|
-
if (x === embedded)
|
|
97
|
-
break;
|
|
98
|
-
try {
|
|
99
|
-
ret[x] = JSON.parse(await (0, promises_1.readFile)(`hfs-lang-${x}.json`, 'utf8'));
|
|
100
|
-
}
|
|
101
|
-
catch (_a) {
|
|
102
|
-
do {
|
|
103
|
-
x = x.substring(0, x.lastIndexOf('-'));
|
|
104
|
-
} while (x && langs.includes(x));
|
|
105
|
-
if (x) {
|
|
106
|
-
langs[i] = x; // overwrite and retry
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
i++;
|
|
111
|
-
}
|
|
112
|
-
return ret;
|
|
113
|
-
}
|
|
114
89
|
};
|
|
115
90
|
function notifyClient(ctx, name, data) {
|
|
116
91
|
const { notificationChannel } = ctx.query;
|
package/src/lang.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EMBEDDED_TRANSLATIONS = exports.getLangData = exports.file2code = exports.code2file = void 0;
|
|
7
|
+
const misc_1 = require("./misc");
|
|
8
|
+
const promises_1 = require("fs/promises");
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
const watchLoad_1 = require("./watchLoad");
|
|
11
|
+
const PREFIX = 'hfs-lang-';
|
|
12
|
+
const SUFFIX = '.json';
|
|
13
|
+
const EMBEDDED_LANGUAGE = 'en';
|
|
14
|
+
function code2file(code) {
|
|
15
|
+
return PREFIX + code.toLowerCase() + SUFFIX;
|
|
16
|
+
}
|
|
17
|
+
exports.code2file = code2file;
|
|
18
|
+
function file2code(fn) {
|
|
19
|
+
return fn.slice(PREFIX.length, -SUFFIX.length);
|
|
20
|
+
}
|
|
21
|
+
exports.file2code = file2code;
|
|
22
|
+
async function getLangData(ctx) {
|
|
23
|
+
if (forceLangData)
|
|
24
|
+
return forceLangData;
|
|
25
|
+
const ret = {};
|
|
26
|
+
const csv = String(ctx.query.lang || '') || ctx.get('Accept-Language') || '';
|
|
27
|
+
const langs = (0, misc_1.wantArray)(csv.split(',').map(x => x.toLowerCase()));
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < langs.length) {
|
|
30
|
+
let k = langs[i] || ''; // shut up ts
|
|
31
|
+
if (!k || k === EMBEDDED_LANGUAGE)
|
|
32
|
+
break;
|
|
33
|
+
try {
|
|
34
|
+
ret[k] = JSON.parse(await (0, promises_1.readFile)(`hfs-lang-${k}.json`, 'utf8'));
|
|
35
|
+
}
|
|
36
|
+
catch (_a) {
|
|
37
|
+
if (k in exports.EMBEDDED_TRANSLATIONS)
|
|
38
|
+
ret[k] = exports.EMBEDDED_TRANSLATIONS[k];
|
|
39
|
+
else {
|
|
40
|
+
do {
|
|
41
|
+
k = k.substring(0, k.lastIndexOf('-'));
|
|
42
|
+
} while (k && langs.includes(k));
|
|
43
|
+
if (k) {
|
|
44
|
+
langs[i] = k; // overwrite and retry
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
return ret;
|
|
52
|
+
}
|
|
53
|
+
exports.getLangData = getLangData;
|
|
54
|
+
let forceLangData;
|
|
55
|
+
let undo;
|
|
56
|
+
(0, config_1.defineConfig)('force_lang', '', v => {
|
|
57
|
+
undo === null || undo === void 0 ? void 0 : undo();
|
|
58
|
+
forceLangData = undefined;
|
|
59
|
+
if (!v)
|
|
60
|
+
return;
|
|
61
|
+
const res = (0, watchLoad_1.watchLoad)(code2file(v), data => {
|
|
62
|
+
forceLangData = { [v]: JSON.parse(data) };
|
|
63
|
+
});
|
|
64
|
+
undo = res.unwatch;
|
|
65
|
+
});
|
|
66
|
+
const hfs_lang_it_json_1 = __importDefault(require("./langs/hfs-lang-it.json"));
|
|
67
|
+
const hfs_lang_zh_json_1 = __importDefault(require("./langs/hfs-lang-zh.json"));
|
|
68
|
+
const hfs_lang_ru_json_1 = __importDefault(require("./langs/hfs-lang-ru.json"));
|
|
69
|
+
const hfs_lang_sr_json_1 = __importDefault(require("./langs/hfs-lang-sr.json"));
|
|
70
|
+
const hfs_lang_ko_json_1 = __importDefault(require("./langs/hfs-lang-ko.json"));
|
|
71
|
+
exports.EMBEDDED_TRANSLATIONS = {
|
|
72
|
+
it: hfs_lang_it_json_1.default,
|
|
73
|
+
zh: hfs_lang_zh_json_1.default,
|
|
74
|
+
ru: hfs_lang_ru_json_1.default,
|
|
75
|
+
sr: hfs_lang_sr_json_1.default,
|
|
76
|
+
ko: hfs_lang_ko_json_1.default,
|
|
77
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Massimo Melina",
|
|
3
|
+
"version": 1.5,
|
|
4
|
+
"hfs_version": "0.43.0",
|
|
5
|
+
"translate": {
|
|
6
|
+
"Select": "Seleziona",
|
|
7
|
+
"n_files": "{n} file",
|
|
8
|
+
"n_folders": "{n,plural, one{# cartella} other{# cartelle}}",
|
|
9
|
+
"filter_count": "{n,plural, one{# filtrato} other{# filtrati}}",
|
|
10
|
+
"select_count": "{n,plural, one{# selezionato} other{# selezionati}}",
|
|
11
|
+
"filter_placeholder": "Digita qui per filtrare la lista dei file",
|
|
12
|
+
"Select some files": "Solo alcuni file",
|
|
13
|
+
"zip_checkboxes": "Spunta le caselle per selezionare i file desiderati, e poi clicca di nuovo su Zip",
|
|
14
|
+
"zip_tooltip_selected": "Scarica gli elementi selezionati come singolo file zip",
|
|
15
|
+
"zip_tooltip_whole": "Scarica intera lista (non considera il filtro) come singolo file zip. Se selezioni alcuni elementi, solo quelli saranno inclusi.",
|
|
16
|
+
"zip_confirm_search": "Scarica TUTTI i risultati di questa ricerca come file zip?",
|
|
17
|
+
"zip_confirm_folder": "Scarica l'INTERA artella come file zip?",
|
|
18
|
+
"select_tooltip": "Selezionare serve con la funziona Zip e per cancellare (dove disponibile), ma puoi anche filtrare la lista",
|
|
19
|
+
"delete_hint": "Per poter cancellare bisogna prima cliccare Seleziona",
|
|
20
|
+
"delete_confirm": "Cancellare {n,plural, one{l'elemento} =8 =11 {gli # elementi} other{i # elementi}}?",
|
|
21
|
+
"delete_completed": "Cancellazione: {n} riusciti",
|
|
22
|
+
"delete_failed": ", {n,plural, one{# fallito} other{# falliti}}",
|
|
23
|
+
"delete_select": "Seleziona qualcosa per poter cancellare",
|
|
24
|
+
"Delete": "Cancella",
|
|
25
|
+
"Options": "Opzioni",
|
|
26
|
+
"Search": "Cerca",
|
|
27
|
+
"search_msg": "Cerca in questa cartella e cartelle sottostanti",
|
|
28
|
+
"Searching": "Cercando",
|
|
29
|
+
"Searched": "Risultati ricerca",
|
|
30
|
+
"Clear search": "Annulla ricerca",
|
|
31
|
+
"Interrupted": "interrotto",
|
|
32
|
+
"stopped_before": "Ricerca interrotta prima di trovare risultati",
|
|
33
|
+
"empty_list": "Non c'è niente qui",
|
|
34
|
+
"filter_none": "Nessun elemento corrisponde al filtro impostato",
|
|
35
|
+
"Login": "Entra",
|
|
36
|
+
"Username": "Nome utente",
|
|
37
|
+
"Continue": "Avanti",
|
|
38
|
+
"login_untrusted": "Login interrotto: server non affidabile",
|
|
39
|
+
"login_bad_credentials": "Nome utente o password non validi",
|
|
40
|
+
"login_bad_cookies": "Login interrotto: non funzionano i cookies",
|
|
41
|
+
"User panel": "Pannello utente",
|
|
42
|
+
"Change password": "Cambia password",
|
|
43
|
+
"enter_pass": "Inserisci nuova password",
|
|
44
|
+
"enter_pass2": "Ripeti la stessa password",
|
|
45
|
+
"pass2_mismatch": "Hai scritto 2 volte la password ma non sono uguali.",
|
|
46
|
+
"password_changed": "La password è stata cambiata",
|
|
47
|
+
"Logout": "Esci",
|
|
48
|
+
"connection error": "errore di connessione",
|
|
49
|
+
"Full timestamp:": "Data e ora:",
|
|
50
|
+
"Search was interrupted": "Ricerca interrotta",
|
|
51
|
+
"Stop list": "Interrompi",
|
|
52
|
+
"upload_starting": "Ora dovrebbe iniziare il download",
|
|
53
|
+
"wrong_account": "L'account {u} non ha accesso a questa cartella, prova con un altro",
|
|
54
|
+
"no_upload_here": "Non hai il permesso di uplodare in questa cartella",
|
|
55
|
+
"Create folder": "Crea cartella",
|
|
56
|
+
"Pick files": "Scegli file",
|
|
57
|
+
"Pick folder": "Scegli una cartella",
|
|
58
|
+
"send_files": "Invia {n} file, {size}",
|
|
59
|
+
"Clear": "Azzera",
|
|
60
|
+
"failed_upload": "Upload fallito per {name}",
|
|
61
|
+
"confirm_resume": "Vuoi riprendere questo upload?",
|
|
62
|
+
"file too large": "file troppo grande",
|
|
63
|
+
"Enter folder name": "Inserisci nome cartella",
|
|
64
|
+
"Successfully created": "Creazione riuscita",
|
|
65
|
+
"enter_folder": "Accedi alla nuova cartella",
|
|
66
|
+
"folder_exists": "Questo nome esiste già",
|
|
67
|
+
"Sort by": "Ordinamento",
|
|
68
|
+
"name": "nome",
|
|
69
|
+
"extension": "estensione",
|
|
70
|
+
"size": "dimensioni",
|
|
71
|
+
"time": "data/ora",
|
|
72
|
+
"Invert order": "Inverti ordine",
|
|
73
|
+
"Folders first": "Prima le cartelle",
|
|
74
|
+
"Numeric names": "Rispetta numeri all'inizio",
|
|
75
|
+
"theme:": "tema:",
|
|
76
|
+
"auto": "automatico",
|
|
77
|
+
"light": "chiaro",
|
|
78
|
+
"dark": "scuro",
|
|
79
|
+
"parent folder": "cartella superiore",
|
|
80
|
+
"Confirm": "Conferma",
|
|
81
|
+
"Don't": "Annulla",
|
|
82
|
+
"Warning": "Attenzione",
|
|
83
|
+
"Error": "Errore",
|
|
84
|
+
"Unauthorized": "Non autorizzato",
|
|
85
|
+
"Forbidden": "Vietato",
|
|
86
|
+
"Not found": "Non trovato",
|
|
87
|
+
"Server error": "Errore del server",
|
|
88
|
+
"upload_concluded": "Upload concluso",
|
|
89
|
+
"upload_finished": "{n,plural, one{# riuscito} other{# riusciti}} ({size})",
|
|
90
|
+
"upload_errors": "{n,plural, one{# fallito} other{# falliti}}",
|
|
91
|
+
"upload_file_rejected": "Alcuni file non sono stati accettati",
|
|
92
|
+
"download counter": "download conteggiati",
|
|
93
|
+
"File menu": "Menu file",
|
|
94
|
+
"Folder menu": "Menu cartella",
|
|
95
|
+
"Name": "Nome",
|
|
96
|
+
"file_open": "Apri",
|
|
97
|
+
"Download": "Download",
|
|
98
|
+
"Missing permission": "Permesso mancante"
|
|
99
|
+
}
|
|
100
|
+
}
|