hfs 3.4.0-alpha1 → 3.4.0-alpha2
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/admin/assets/AccountsPage-CoiPofEv.js +4 -0
- package/admin/assets/{ConfigFilePage-D5TZZyMR.js → ConfigFilePage-jUH2feqi.js} +1 -1
- package/admin/assets/{CustomHtmlPage-DV2-ufWZ.js → CustomHtmlPage-Pl_AJZrl.js} +1 -1
- package/admin/assets/{InternetPage-DT3wSQYQ.js → InternetPage-DIcAwhFk.js} +1 -1
- package/admin/assets/{LangPage-DbXCv7wR.js → LangPage-BwVY8QRD.js} +1 -1
- package/admin/assets/{LogsPage-DmAxUZpu.js → LogsPage-CcjS12cP.js} +1 -1
- package/admin/assets/{MonitorPage-D7ydOeq_.js → MonitorPage-DMVFXyMe.js} +1 -1
- package/admin/assets/OptionsPage-BB40ax9G.js +3 -0
- package/admin/assets/{PluginsPage-BYzfwuvg.js → PluginsPage-CC2dINcb.js} +1 -1
- package/admin/assets/{icons-CfBG9vvB.js → icons-C_ux1zLA.js} +1 -1
- package/admin/assets/{index-CVCnYMoB.js → index-BSSQOMG_.js} +4 -4
- package/admin/assets/{routing-C4l8rnk_.js → routing-Be72yFmd.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/{dist-legacy-CKczq_fx.js → dist-legacy-D7Dg5w3L.js} +1 -1
- package/frontend/assets/index-legacy-CnazC3Dj.js +1 -0
- package/frontend/assets/{lib-legacy-D2tyILlj.js → lib-legacy-BxFHpdER.js} +1 -1
- package/frontend/assets/{sha512-legacy-CFcDeiiP.js → sha512-legacy-C8xoQBnP.js} +1 -1
- package/frontend/index.html +1 -1
- package/npm-shrinkwrap.json +66 -66
- package/package.json +1 -1
- package/plugins/antibrute/plugin.js +72 -18
- package/src/admin-langs/embedded.js +2 -2
- package/src/admin-langs/hfs-admin-lang-ar.json +34 -26
- package/src/admin-langs/hfs-admin-lang-bg.json +35 -27
- package/src/admin-langs/hfs-admin-lang-de.json +37 -29
- package/src/admin-langs/hfs-admin-lang-el.json +38 -30
- package/src/admin-langs/hfs-admin-lang-en.json +8 -0
- package/src/admin-langs/hfs-admin-lang-es.json +37 -29
- package/src/admin-langs/hfs-admin-lang-fi.json +100 -92
- package/src/admin-langs/hfs-admin-lang-fr.json +39 -31
- package/src/admin-langs/hfs-admin-lang-hu.json +37 -29
- package/src/admin-langs/hfs-admin-lang-it.json +35 -27
- package/src/admin-langs/hfs-admin-lang-ja.json +36 -28
- package/src/admin-langs/hfs-admin-lang-ko.json +39 -31
- package/src/admin-langs/hfs-admin-lang-lt.json +35 -27
- package/src/admin-langs/hfs-admin-lang-ms.json +36 -28
- package/src/admin-langs/hfs-admin-lang-nl.json +36 -28
- package/src/admin-langs/hfs-admin-lang-pl.json +36 -28
- package/src/admin-langs/hfs-admin-lang-pt-br.json +36 -28
- package/src/admin-langs/hfs-admin-lang-ro.json +37 -29
- package/src/admin-langs/hfs-admin-lang-ru.json +35 -27
- package/src/admin-langs/hfs-admin-lang-sr-latn.json +35 -27
- package/src/admin-langs/hfs-admin-lang-sr.json +39 -31
- package/src/admin-langs/hfs-admin-lang-th.json +35 -27
- package/src/admin-langs/hfs-admin-lang-tr.json +38 -30
- package/src/admin-langs/hfs-admin-lang-uk.json +35 -27
- package/src/admin-langs/hfs-admin-lang-vi.json +35 -27
- package/src/admin-langs/hfs-admin-lang-zh-hant.json +1149 -0
- package/src/admin-langs/hfs-admin-lang-zh.json +728 -720
- package/src/adminApis.js +2 -1
- package/src/adminLang.js +8 -7
- package/src/api.accounts.js +9 -4
- package/src/api.auth.js +11 -3
- package/src/api.vfs.js +3 -2
- package/src/auth.js +25 -14
- package/src/consoleLog.js +4 -0
- package/src/const.js +1 -1
- package/src/cross-const.js +1 -1
- package/src/cross.js +1 -1
- package/src/customHtml.js +3 -2
- package/src/deletePermission.js +120 -0
- package/src/events.js +6 -0
- package/src/index.js +3 -2
- package/src/lang.js +6 -1
- package/src/langs/embedded.js +2 -2
- package/src/langs/hfs-lang-ar.json +2 -1
- package/src/langs/hfs-lang-bg.json +2 -1
- package/src/langs/hfs-lang-de.json +2 -1
- package/src/langs/hfs-lang-el.json +2 -1
- package/src/langs/hfs-lang-en.json +2 -1
- package/src/langs/hfs-lang-es.json +2 -1
- package/src/langs/hfs-lang-fi.json +2 -1
- package/src/langs/hfs-lang-fr.json +2 -1
- package/src/langs/hfs-lang-hu.json +2 -1
- package/src/langs/hfs-lang-it.json +2 -1
- package/src/langs/hfs-lang-ja.json +2 -1
- package/src/langs/hfs-lang-ko.json +2 -1
- package/src/langs/hfs-lang-lt.json +2 -1
- package/src/langs/hfs-lang-ms.json +2 -1
- package/src/langs/hfs-lang-nl.json +2 -1
- package/src/langs/hfs-lang-pl.json +2 -1
- package/src/langs/hfs-lang-pt-br.json +2 -1
- package/src/langs/hfs-lang-ro.json +2 -1
- package/src/langs/hfs-lang-ru.json +2 -1
- package/src/langs/hfs-lang-sr-latn.json +2 -1
- package/src/langs/hfs-lang-sr.json +2 -1
- package/src/langs/hfs-lang-th.json +2 -1
- package/src/langs/hfs-lang-tr.json +2 -1
- package/src/langs/hfs-lang-uk.json +2 -1
- package/src/langs/hfs-lang-vi.json +2 -1
- package/src/langs/hfs-lang-zh-hant.json +189 -0
- package/src/langs/hfs-lang-zh.json +96 -98
- package/src/listen.js +8 -4
- package/src/middlewares.js +31 -28
- package/src/perm.js +26 -4
- package/src/plugins.js +2 -2
- package/src/serveFile.js +5 -9
- package/src/serveGuiAndSharedFiles.js +9 -8
- package/src/serveGuiFiles.js +2 -2
- package/src/sessionStamp.js +53 -0
- package/src/upload.js +1 -1
- package/src/uploadOwners.js +4 -2
- package/src/vfs.js +80 -33
- package/src/webdav.js +2 -2
- package/admin/assets/AccountsPage-C6J2hRcY.js +0 -4
- package/admin/assets/OptionsPage-DpN3ajRm.js +0 -3
- package/frontend/assets/index-legacy-ZYae30AY.js +0 -1
- package/src/admin-langs/hfs-admin-lang-zh-tw.json +0 -1141
- package/src/langs/hfs-lang-zh-tw.json +0 -187
|
@@ -1,191 +1,189 @@
|
|
|
1
1
|
{
|
|
2
|
-
"author": "
|
|
3
|
-
"version": 1.
|
|
4
|
-
"hfs_version": "0.57.5",
|
|
2
|
+
"author": "Hug3O",
|
|
3
|
+
"version": 1.2,
|
|
5
4
|
"translate": {
|
|
6
5
|
"Select": "选择",
|
|
7
|
-
"n_files": "{n}
|
|
8
|
-
"n_folders": "{n}
|
|
9
|
-
"filter_count": "{n}
|
|
10
|
-
"select_count": "{n}
|
|
11
|
-
"filter_placeholder": "
|
|
6
|
+
"n_files": "{n,plural,one{# 文件} other{# 文件}}",
|
|
7
|
+
"n_folders": "{n,plural,one{# 文件夹} other{# 文件夹}}",
|
|
8
|
+
"filter_count": "{n,plural, one{# 个已筛选} other{# 个已筛选}}",
|
|
9
|
+
"select_count": "{n,plural, one{# 个已选择} other{# 个已选择}}",
|
|
10
|
+
"filter_placeholder": "在此输入以筛选下方列表",
|
|
12
11
|
"Select some files": "选择一些文件",
|
|
13
|
-
"zip_checkboxes": "
|
|
14
|
-
"zip_tooltip_selected": "
|
|
15
|
-
"zip_tooltip_whole": "
|
|
16
|
-
"zip_confirm_search": "
|
|
17
|
-
"zip_confirm_folder": "
|
|
18
|
-
"select_tooltip": "
|
|
19
|
-
"delete_hint": "
|
|
20
|
-
"delete_confirm": "
|
|
21
|
-
"delete_completed": "
|
|
22
|
-
"delete_failed": "
|
|
12
|
+
"zip_checkboxes": "使用复选框选择文件,然后可以再次使用 Zip",
|
|
13
|
+
"zip_tooltip_selected": "将选中的元素作为单个 zip 文件下载",
|
|
14
|
+
"zip_tooltip_whole": "将整个列表(未筛选)作为单个 zip 文件下载。如果你选择了一些元素,则只下载那些元素。",
|
|
15
|
+
"zip_confirm_search": "将此搜索的所有结果下载为 ZIP 压缩包?",
|
|
16
|
+
"zip_confirm_folder": "将整个文件夹打包为ZIP再进行下载?",
|
|
17
|
+
"select_tooltip": "选择适用于“Zip”和“删除”(如果可用),但你也可以筛选列表",
|
|
18
|
+
"delete_hint": "要删除,请先点击“选择”",
|
|
19
|
+
"delete_confirm": "删除 {n,plural, one{# 个项目} other{# 个项目}}?",
|
|
20
|
+
"delete_completed": "删除:已完成 {n} 个",
|
|
21
|
+
"delete_failed": ",{n,plural, one{# 个失败} other{# 个失败}}",
|
|
23
22
|
"delete_select": "选择要删除的内容",
|
|
24
23
|
"Delete": "删除",
|
|
25
24
|
"Options": "选项",
|
|
26
25
|
"Search": "搜索",
|
|
27
|
-
"Zip": "
|
|
28
|
-
"search_msg": "
|
|
29
|
-
"Searching": "
|
|
30
|
-
"Searched": "
|
|
31
|
-
"Clear search": "
|
|
32
|
-
"Interrupted": "
|
|
33
|
-
"stopped_before": "
|
|
34
|
-
"empty_list": "
|
|
35
|
-
"filter_none": "
|
|
36
|
-
"
|
|
26
|
+
"Zip": "Zip",
|
|
27
|
+
"search_msg": "搜索此文件夹及子文件夹",
|
|
28
|
+
"Searching": "正在搜索",
|
|
29
|
+
"Searched": "已搜索",
|
|
30
|
+
"Clear search": "清除搜索",
|
|
31
|
+
"Interrupted": "已中断",
|
|
32
|
+
"stopped_before": "在找到任何内容之前已停止",
|
|
33
|
+
"empty_list": "这里什么都没有",
|
|
34
|
+
"filter_none": "没有与此筛选条件匹配的内容",
|
|
35
|
+
"alpha_idx": "字母索引",
|
|
36
|
+
"alpha_idx_button": "AZ",
|
|
37
|
+
"Admin-panel": "后台管理",
|
|
37
38
|
"Login": "登录",
|
|
38
39
|
"Username": "用户名",
|
|
39
40
|
"Password": "密码",
|
|
40
|
-
"login_untrusted": "
|
|
41
|
-
"login_bad_credentials": "
|
|
42
|
-
"confirm_url_login": "此链接正尝试以“{username}
|
|
43
|
-
"login_bad_cookies": "Cookie
|
|
41
|
+
"login_untrusted": "登录已中止:无法信任服务器身份",
|
|
42
|
+
"login_bad_credentials": "凭据无效",
|
|
43
|
+
"confirm_url_login": "此链接正尝试以“{username}”身份登录你",
|
|
44
|
+
"login_bad_cookies": "Cookie 无法正常工作 - 登录失败",
|
|
44
45
|
"User panel": "用户面板",
|
|
45
46
|
"Change password": "更改密码",
|
|
46
47
|
"enter_pass": "输入新密码",
|
|
47
|
-
"enter_pass2": "
|
|
48
|
-
"pass2_mismatch": "
|
|
49
|
-
"password_changed": "
|
|
50
|
-
"Logout": "
|
|
48
|
+
"enter_pass2": "再次输入相同的新密码",
|
|
49
|
+
"pass2_mismatch": "你输入的第二个密码与第一个不匹配。过程已中止。",
|
|
50
|
+
"password_changed": "密码已更改",
|
|
51
|
+
"Logout": "退出登录",
|
|
51
52
|
"connection error": "连接错误",
|
|
52
53
|
"Full timestamp:": "完整时间戳:",
|
|
53
54
|
"Search was interrupted": "搜索已中断",
|
|
54
55
|
"Stop list": "停止列表",
|
|
55
|
-
"download_starting": "
|
|
56
|
-
"wrong_account": "
|
|
57
|
-
"no_upload_here": "
|
|
56
|
+
"download_starting": "你的下载现在应开始",
|
|
57
|
+
"wrong_account": "账户 {u} 没有访问权限,请尝试其他账户",
|
|
58
|
+
"no_upload_here": "当前文件夹没有上传权限",
|
|
58
59
|
"Create folder": "创建文件夹",
|
|
59
60
|
"Pick files": "选择文件",
|
|
60
61
|
"Pick folder": "选择文件夹",
|
|
61
|
-
"ready_to_upload": "{n,plural,other{# 个文件}},{size},准备上传",
|
|
62
|
+
"ready_to_upload": "{n,plural,one{# 个文件} other{# 个文件}},{size},准备上传",
|
|
62
63
|
"Send": "发送",
|
|
63
64
|
"Clear": "清除",
|
|
64
65
|
"failed_upload": "无法上传 {name}",
|
|
65
|
-
"confirm_resume": "是否重命名?",
|
|
66
66
|
"file too large": "文件过大",
|
|
67
67
|
"Enter folder name": "输入文件夹名称",
|
|
68
68
|
"Successfully created": "创建成功",
|
|
69
|
-
"enter_folder": "
|
|
70
|
-
"folder_exists": "
|
|
71
|
-
"Sort by": "
|
|
72
|
-
"name": "
|
|
73
|
-
"extension": "
|
|
69
|
+
"enter_folder": "输入文件夹",
|
|
70
|
+
"folder_exists": "同名文件夹已存在",
|
|
71
|
+
"Sort by:": "排序:{by}",
|
|
72
|
+
"name": "文件名称",
|
|
73
|
+
"extension": "文件后缀",
|
|
74
74
|
"size": "文件大小",
|
|
75
|
-
"time": "
|
|
76
|
-
"Invert order": "
|
|
77
|
-
"Folders first": "
|
|
75
|
+
"time": "修改时间",
|
|
76
|
+
"Invert order": "倒序排列",
|
|
77
|
+
"Folders first": "文件夹先",
|
|
78
78
|
"Numeric names": "数字名称",
|
|
79
79
|
"theme:": "主题:",
|
|
80
80
|
"auto": "自动",
|
|
81
|
-
"light": "
|
|
82
|
-
"dark": "
|
|
83
|
-
"parent folder": "
|
|
81
|
+
"light": "浅色",
|
|
82
|
+
"dark": "深色",
|
|
83
|
+
"parent folder": "父文件夹",
|
|
84
84
|
"home": "主页",
|
|
85
85
|
"Continue": "继续",
|
|
86
86
|
"Confirm": "确认",
|
|
87
|
-
"Don't": "
|
|
87
|
+
"Don't": "不要",
|
|
88
88
|
"Warning": "警告",
|
|
89
89
|
"Error": "错误",
|
|
90
90
|
"Info": "信息",
|
|
91
91
|
"Unauthorized": "未授权",
|
|
92
|
-
"Forbidden": "
|
|
92
|
+
"Forbidden": "禁止",
|
|
93
93
|
"Not found": "未找到",
|
|
94
94
|
"Server error": "服务器错误",
|
|
95
95
|
"Upload": "上传",
|
|
96
96
|
"upload_concluded": "上传已结束:",
|
|
97
|
-
"upload_finished": "
|
|
98
|
-
"upload_errors": "
|
|
99
|
-
"upload_file_rejected": "
|
|
100
|
-
"delete_unfinished_upload": "
|
|
97
|
+
"upload_finished": "{n} 个已完成({size})",
|
|
98
|
+
"upload_errors": "{n} 个失败",
|
|
99
|
+
"upload_file_rejected": "某些文件未被接受",
|
|
100
|
+
"delete_unfinished_upload": "删除未完成的上传?",
|
|
101
101
|
"File menu": "文件菜单",
|
|
102
102
|
"Folder menu": "文件夹菜单",
|
|
103
103
|
"Name": "名称",
|
|
104
104
|
"file_open": "打开",
|
|
105
105
|
"Download": "下载",
|
|
106
|
-
"Missing permission": "
|
|
106
|
+
"Missing permission": "缺少权限",
|
|
107
107
|
"Reload": "重新加载",
|
|
108
108
|
"Get list": "获取列表",
|
|
109
|
-
"Skip existing files": "
|
|
109
|
+
"Skip existing files": "跳过现有文件",
|
|
110
110
|
"Size": "大小",
|
|
111
|
-
"Timestamp": "
|
|
112
|
-
"Show": "
|
|
111
|
+
"Timestamp": "修改时间",
|
|
112
|
+
"Show": "显示",
|
|
113
113
|
"Loading failed": "加载失败",
|
|
114
|
+
"Download limit reached": "已达到下载数量上限",
|
|
115
|
+
"Retry": "重试",
|
|
114
116
|
"Rename": "重命名",
|
|
115
117
|
"Tiles mode:": "平铺模式:",
|
|
116
118
|
"off": "关闭",
|
|
117
119
|
"Operation successful": "操作成功",
|
|
118
120
|
"Uploader": "上传者",
|
|
119
|
-
"Download counter": "
|
|
121
|
+
"Download counter": "下载计数器",
|
|
120
122
|
"Switch zoom mode": "切换缩放模式",
|
|
121
123
|
"Full screen": "全屏",
|
|
122
124
|
"File Show help": "文件显示帮助",
|
|
123
|
-
"showHelpMain": "
|
|
125
|
+
"showHelpMain": "你可以使用键盘执行一些操作:",
|
|
124
126
|
"showHelp_←/→": "←/→",
|
|
125
127
|
"showHelp_↑/↓": "↑/↓",
|
|
126
128
|
"showHelp_space": "空格",
|
|
127
129
|
"showHelp_←/→_body": "转到上一个/下一个文件",
|
|
128
|
-
"showHelp_↑/↓_body": "
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"in_queue": "{n} 个在队列中",
|
|
135
|
-
"enter_comment": "对此文件的评论",
|
|
136
|
-
"Comment": "评论",
|
|
137
|
-
"upload_dd_hint": "您可以将文件拖放到文件列表中进行上传",
|
|
130
|
+
"showHelp_↑/↓_body": "滚动长图片",
|
|
131
|
+
"Destination": "目标",
|
|
132
|
+
"in_queue": "队列中有 {n} 个",
|
|
133
|
+
"enter_comment": "为 {name} 添加注释",
|
|
134
|
+
"Comment": "注释",
|
|
135
|
+
"upload_dd_hint": "你可以通过将文件拖放到文件列表上来上传文件",
|
|
138
136
|
"Upload not available": "上传不可用",
|
|
139
137
|
"Cut": "剪切",
|
|
140
|
-
"n_items": "{n,plural, other{# 个项目}}",
|
|
141
|
-
"move_results": "{good}
|
|
142
|
-
"after_cut": "
|
|
138
|
+
"n_items": "{n,plural, one{# 个项目} other{# 个项目}}",
|
|
139
|
+
"move_results": "{good} 个已移动{bad,plural, =0{} other{,# 个失败}}",
|
|
140
|
+
"after_cut": "你的选择现在已在剪贴板中。\n转到目标文件夹进行粘贴。",
|
|
143
141
|
"Cancel clipboard": "取消剪贴板",
|
|
144
142
|
"to_clipboard_source": "返回源文件夹",
|
|
145
143
|
"Paste": "粘贴",
|
|
146
|
-
"clipboard_list": "
|
|
144
|
+
"clipboard_list": "剪贴板中的项目:",
|
|
147
145
|
"Close": "关闭",
|
|
148
146
|
"Folder": "文件夹",
|
|
149
147
|
"Web page": "网页",
|
|
150
148
|
"Link": "链接",
|
|
151
149
|
"Auto-play": "自动播放",
|
|
152
|
-
"autoplay_seconds": "
|
|
150
|
+
"autoplay_seconds": "图片等待秒数",
|
|
153
151
|
"Select all": "全选",
|
|
154
|
-
"go_first": "
|
|
155
|
-
"go_last": "
|
|
152
|
+
"go_first": "转到第一项",
|
|
153
|
+
"go_last": "转到最后一项",
|
|
156
154
|
"Shuffle": "随机播放",
|
|
157
155
|
"Repeat": "重复",
|
|
158
|
-
"showHelpListShortcut": "在文件列表中,按住 {key}
|
|
156
|
+
"showHelpListShortcut": "在文件列表中,按住 {key} 点击可显示",
|
|
159
157
|
"Invalid value": "无效值",
|
|
160
|
-
"upload_interrupted": "{n}
|
|
158
|
+
"upload_interrupted": "{n} 个已中断",
|
|
161
159
|
"Overwrite policy": "覆盖策略",
|
|
162
160
|
"Rename to avoid overwriting": "重命名以避免覆盖",
|
|
163
|
-
"Overwrite existing files": "
|
|
161
|
+
"Overwrite existing files": "覆盖现有文件",
|
|
164
162
|
"Menu": "菜单",
|
|
165
|
-
"clipboard": "
|
|
166
|
-
"to_clipboard_source_tooltip": "
|
|
163
|
+
"clipboard": "剪贴板({content})",
|
|
164
|
+
"to_clipboard_source_tooltip": "转到剪贴板内容所在的文件夹",
|
|
167
165
|
"more_items": "还有 {n} 个项目",
|
|
168
|
-
"Show details": "
|
|
166
|
+
"Show details": "显示详情",
|
|
169
167
|
"upload_conflict": "已存在",
|
|
170
168
|
"Logged in": "已登录",
|
|
171
|
-
"Logged out": "
|
|
169
|
+
"Logged out": "已退出登录",
|
|
172
170
|
"Cancel": "取消",
|
|
173
|
-
"allow_session_ip_change": "
|
|
174
|
-
"focus_hint": "
|
|
171
|
+
"allow_session_ip_change": "允许此会话期间 IP 变更",
|
|
172
|
+
"focus_hint": "通过在键盘上输入,你可以搜索并聚焦列表中的项目。",
|
|
175
173
|
"copy_links": "复制链接",
|
|
176
|
-
"Calculate": "
|
|
174
|
+
"Calculate": "统计",
|
|
177
175
|
"Creation": "创建时间",
|
|
178
|
-
"creation": "
|
|
179
|
-
"required_change_password": "
|
|
176
|
+
"creation": "创建时间",
|
|
177
|
+
"required_change_password": "你需要更改密码",
|
|
180
178
|
"Wildcards": "通配符",
|
|
181
|
-
"Considering resume": "
|
|
179
|
+
"Considering resume": "考虑恢复",
|
|
182
180
|
"Yes": "是",
|
|
183
181
|
"No": "否",
|
|
184
|
-
"esc_again": "
|
|
182
|
+
"esc_again": "按两次 ESC 关闭",
|
|
185
183
|
"insufficient storage": "存储空间不足",
|
|
186
|
-
"Account disabled": "
|
|
184
|
+
"Account disabled": "账户已停用",
|
|
187
185
|
"upload_name": "使用新名称上传",
|
|
188
|
-
"Uploading": "
|
|
189
|
-
"proxy_413": "
|
|
186
|
+
"Uploading": "正在上传",
|
|
187
|
+
"proxy_413": "请检查代理服务器上的此限制"
|
|
190
188
|
}
|
|
191
189
|
}
|
package/src/listen.js
CHANGED
|
@@ -70,10 +70,16 @@ const consoleLog_1 = require("./consoleLog");
|
|
|
70
70
|
const first_1 = require("./first");
|
|
71
71
|
const fileAttr_1 = require("./fileAttr");
|
|
72
72
|
const uploadOwners_1 = require("./uploadOwners");
|
|
73
|
+
const sessionStamp_1 = require("./sessionStamp");
|
|
73
74
|
let httpSrv;
|
|
74
75
|
let httpsSrv;
|
|
75
76
|
// the update relaunch can keep a bridge process alive, so we proactively close listeners here to release ports before the next binary binds; do it before (5) the storage file is closed, because sockets write there
|
|
76
|
-
(0, first_1.onProcessExit)(() =>
|
|
77
|
+
(0, first_1.onProcessExit)(() => {
|
|
78
|
+
const stopped = Promise.all([stopServer(httpSrv), stopServer(httpsSrv)]);
|
|
79
|
+
for (const { socket } of (0, connections_1.getConnections)()) // closeAllConnections misses TCP sockets awaiting TLS and connections upgraded by plugins
|
|
80
|
+
socket.destroy();
|
|
81
|
+
return stopped;
|
|
82
|
+
}, 5);
|
|
77
83
|
const openBrowserAtStart = (0, config_1.defineConfig)(misc_1.CFG.open_browser_at_start, true);
|
|
78
84
|
exports.baseUrl = (0, config_1.defineConfig)(misc_1.CFG.base_url, '', x => /(?<=\/\/)[^\/]+/.exec(x)?.[0]); // compiled is host only
|
|
79
85
|
async function getBaseUrlOrDefault() {
|
|
@@ -88,7 +94,7 @@ const commonServerOptions = {
|
|
|
88
94
|
};
|
|
89
95
|
// these are properties that can be assigned to the server object
|
|
90
96
|
const commonServerAssign = { headersTimeout: 30_000, timeout: misc_1.MINUTE }; // 'headersTimeout' is not recognized by type lib, and 'timeout' is not effective when passed in parameters
|
|
91
|
-
const readyToListen = Promise.all([persistence_1.storedMap.isOpening(), fileAttr_1.fileAttrDb.isOpening(), uploadOwners_1.uploadOwners.isOpening(), events_1.default.once('app')]);
|
|
97
|
+
const readyToListen = Promise.all([persistence_1.storedMap.isOpening(), sessionStamp_1.sessionStampsReady, fileAttr_1.fileAttrDb.isOpening(), uploadOwners_1.uploadOwners.isOpening(), events_1.default.once('app')]);
|
|
92
98
|
const considerHttp = (0, misc_1.debounceAsync)(async () => {
|
|
93
99
|
await readyToListen;
|
|
94
100
|
void stopServer(httpSrv);
|
|
@@ -302,8 +308,6 @@ function stopServer(srv) {
|
|
|
302
308
|
console.debug("Failed to stop server", String(err));
|
|
303
309
|
resolve(err);
|
|
304
310
|
});
|
|
305
|
-
if (first_1.quitting)
|
|
306
|
-
srv.closeAllConnections();
|
|
307
311
|
});
|
|
308
312
|
}
|
|
309
313
|
function getCurrentPort(srv) {
|
package/src/middlewares.js
CHANGED
|
@@ -4,26 +4,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.
|
|
7
|
+
exports.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.blockFilter = exports.prepareConnection = exports.someSecurity = exports.cloudflareDetected = exports.gzipper = exports.sessionDuration = void 0;
|
|
8
8
|
exports.getProxyDetected = getProxyDetected;
|
|
9
9
|
exports.failAllowNet = failAllowNet;
|
|
10
10
|
const koa_compress_1 = __importDefault(require("koa-compress"));
|
|
11
11
|
const const_1 = require("./const");
|
|
12
12
|
const misc_1 = require("./misc");
|
|
13
|
-
const node_crypto_1 = require("node:crypto");
|
|
14
|
-
const lang_1 = require("./lang");
|
|
15
|
-
const i18n_1 = require("./i18n");
|
|
16
|
-
const stream_1 = require("stream");
|
|
17
13
|
const block_1 = require("./block");
|
|
18
14
|
const perm_1 = require("./perm");
|
|
19
15
|
const connections_1 = require("./connections");
|
|
20
16
|
const auth_1 = require("./auth");
|
|
17
|
+
const sessionStamp_1 = require("./sessionStamp");
|
|
21
18
|
const zlib_1 = require("zlib");
|
|
22
19
|
const listen_1 = require("./listen");
|
|
20
|
+
const roots_1 = require("./roots");
|
|
21
|
+
const node_net_1 = require("node:net");
|
|
23
22
|
const config_1 = require("./config");
|
|
24
23
|
const koa_session_1 = __importDefault(require("koa-session"));
|
|
25
24
|
const index_1 = require("./index");
|
|
26
25
|
const events_1 = __importDefault(require("./events"));
|
|
26
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
const lang_1 = require("./lang");
|
|
28
|
+
const i18n_1 = require("./i18n");
|
|
27
29
|
const forceHttps = (0, config_1.defineConfig)(misc_1.CFG.force_https, true);
|
|
28
30
|
(0, config_1.defineConfig)(misc_1.CFG.ignore_proxies, false);
|
|
29
31
|
const allowAuthorizationHeader = (0, config_1.defineConfig)(misc_1.CFG.authorization_header, true);
|
|
@@ -38,26 +40,9 @@ exports.gzipper = (0, koa_compress_1.default)({
|
|
|
38
40
|
return /text|javascript|style/i.test(type);
|
|
39
41
|
},
|
|
40
42
|
});
|
|
41
|
-
const headRequests = async (ctx, next) => {
|
|
42
|
-
const head = ctx.method === 'HEAD';
|
|
43
|
-
if (head)
|
|
44
|
-
ctx.method = 'GET'; // let other middlewares work, so we can collect the size at the end
|
|
45
|
-
await next();
|
|
46
|
-
if (!head || ctx.body === undefined)
|
|
47
|
-
return;
|
|
48
|
-
const { length, status } = ctx.response;
|
|
49
|
-
if (ctx.body)
|
|
50
|
-
ctx.body = stream_1.Readable.from(''); // empty the body for this is a HEAD request. Using Readable avoids koa from trying to set length to 0
|
|
51
|
-
ctx.status = status;
|
|
52
|
-
if (length)
|
|
53
|
-
ctx.response.length = length;
|
|
54
|
-
};
|
|
55
|
-
exports.headRequests = headRequests;
|
|
56
43
|
let proxyDetected;
|
|
57
44
|
const someSecurity = (ctx, next) => {
|
|
58
45
|
enforceSessionIp(ctx);
|
|
59
|
-
if (!ctx.state.skipFilters && (0, block_1.applyBlock)(ctx.socket, ctx.ip))
|
|
60
|
-
return;
|
|
61
46
|
const decodedPath = (0, misc_1.try_)(() => decodeURI(ctx.path));
|
|
62
47
|
if (!decodedPath || (0, misc_1.hasDirTraversal)(decodedPath))
|
|
63
48
|
return;
|
|
@@ -111,6 +96,16 @@ async function sendPage(ctx, titleKey, content) {
|
|
|
111
96
|
body{margin-top:30vh;text-align:center;font-family:sans-serif}a{margin:1em}
|
|
112
97
|
</style></head><body><h1>${(0, misc_1.escapeHTML)(title)}</h1>${content(t)}</body></html>`;
|
|
113
98
|
}
|
|
99
|
+
const prepareConnection = (ctx, next) => {
|
|
100
|
+
// normalize once so auth, filters and logging agree on the same client address
|
|
101
|
+
ctx.request.ip = (0, misc_1.normalizeIp)(ctx.ip);
|
|
102
|
+
ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
103
|
+
(0, connections_1.updateConnectionForCtx)(ctx);
|
|
104
|
+
return next();
|
|
105
|
+
};
|
|
106
|
+
exports.prepareConnection = prepareConnection;
|
|
107
|
+
const blockFilter = (ctx, next) => !ctx.state.skipFilters && (0, block_1.applyBlock)(ctx.socket, ctx.ip) || next();
|
|
108
|
+
exports.blockFilter = blockFilter;
|
|
114
109
|
function enforceSessionIp(ctx) {
|
|
115
110
|
const s = ctx.session;
|
|
116
111
|
if (!s?.username || s[misc_1.ALLOW_SESSION_IP_CHANGE])
|
|
@@ -129,8 +124,7 @@ function getProxyDetected() {
|
|
|
129
124
|
return proxyDetected && { from: proxyDetected.socket.remoteAddress, for: proxyDetected.get('X-Forwarded-For') };
|
|
130
125
|
}
|
|
131
126
|
const prepareState = async (ctx, next) => {
|
|
132
|
-
|
|
133
|
-
ctx.request.ip = (0, misc_1.normalizeIp)(ctx.ip);
|
|
127
|
+
ctx.state.getOrHead = ctx.method === 'GET' || ctx.method === 'HEAD';
|
|
134
128
|
// invalidate before account resolution; someSecurity calls again to bind logins made below
|
|
135
129
|
enforceSessionIp(ctx);
|
|
136
130
|
// rootsMiddleware consults proxy-aware admin access before someSecurity runs
|
|
@@ -148,12 +142,13 @@ const prepareState = async (ctx, next) => {
|
|
|
148
142
|
}
|
|
149
143
|
const s = ctx.session;
|
|
150
144
|
if (s?.username) {
|
|
151
|
-
|
|
145
|
+
const account = (0, perm_1.getAccount)(s.username, false);
|
|
146
|
+
if (!account || !s.stamp || s.stamp !== (0, sessionStamp_1.getSessionStamp)(s.username)) {
|
|
152
147
|
delete s.username;
|
|
148
|
+
delete s.stamp;
|
|
149
|
+
}
|
|
153
150
|
s.maxAge = exports.sessionDuration.compiled();
|
|
154
151
|
}
|
|
155
|
-
// calculate these once and for all
|
|
156
|
-
ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
157
152
|
// explicit credentials and existing sessions must take precedence, so a matching IP cannot override a chosen account
|
|
158
153
|
let via;
|
|
159
154
|
const urlAccount = await urlLogin();
|
|
@@ -178,7 +173,7 @@ const prepareState = async (ctx, next) => {
|
|
|
178
173
|
ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
|
|
179
174
|
}
|
|
180
175
|
ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
|
|
181
|
-
(0, connections_1.updateConnectionForCtx)(ctx);
|
|
176
|
+
(0, connections_1.updateConnectionForCtx)(ctx); // publish the resolved account after the initial pre-auth connection update
|
|
182
177
|
await next();
|
|
183
178
|
async function urlLogin() {
|
|
184
179
|
via = 'url';
|
|
@@ -232,6 +227,14 @@ const prepareState = async (ctx, next) => {
|
|
|
232
227
|
catch { }
|
|
233
228
|
}
|
|
234
229
|
function autoLogin() {
|
|
230
|
+
const host = ctx.host.toLowerCase();
|
|
231
|
+
const hostname = (0, misc_1.normalizeHost)(host);
|
|
232
|
+
// a rebinding page uses the victim's real IP, but retains the attacker's hostname
|
|
233
|
+
if (hostname !== 'localhost' && !(0, node_net_1.isIP)(hostname)
|
|
234
|
+
// URL normalization matches browsers' default-port removal and internationalized domain names
|
|
235
|
+
&& host !== (listen_1.baseUrl.get() && (0, misc_1.try_)(() => new URL(listen_1.baseUrl.get()).host.toLowerCase()))
|
|
236
|
+
&& roots_1.roots.compiled()(host) === undefined)
|
|
237
|
+
return;
|
|
235
238
|
via = 'net';
|
|
236
239
|
// keep the mask direct so group inheritance cannot make identity depend on account order
|
|
237
240
|
return Object.values(perm_1.accounts.get()).find(a => (0, perm_1.accountCanLogin)(a) && a.auto_login_net && (0, misc_1.netMatches)(ctx.ip, a.auto_login_net, true));
|
package/src/perm.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.getAccount = getAccount;
|
|
|
12
12
|
exports.saveSrpInfo = saveSrpInfo;
|
|
13
13
|
exports.createAdmin = createAdmin;
|
|
14
14
|
exports.updateAccount = updateAccount;
|
|
15
|
+
exports.invalidateAccountSessions = invalidateAccountSessions;
|
|
15
16
|
exports.normalizeUsername = normalizeUsername;
|
|
16
17
|
exports.renameAccount = renameAccount;
|
|
17
18
|
exports.addAccount = addAccount;
|
|
@@ -29,6 +30,7 @@ const config_1 = require("./config");
|
|
|
29
30
|
const tssrp6a_1 = require("tssrp6a");
|
|
30
31
|
const events_1 = __importDefault(require("./events"));
|
|
31
32
|
const auth_1 = require("./auth");
|
|
33
|
+
const sessionStamp_1 = require("./sessionStamp");
|
|
32
34
|
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
|
|
33
35
|
function expandUsername(who) {
|
|
34
36
|
return new Set(Array.from(walkActiveAccounts([who]), a => a.username));
|
|
@@ -69,6 +71,7 @@ async function createAdmin(password, username = 'admin') {
|
|
|
69
71
|
}
|
|
70
72
|
const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
|
|
71
73
|
async function updateAccount(account, change) {
|
|
74
|
+
await sessionStamp_1.sessionStampsReady;
|
|
72
75
|
const jsonWas = JSON.stringify(account);
|
|
73
76
|
const { username: usernameWas } = account;
|
|
74
77
|
if (typeof change === 'function')
|
|
@@ -111,8 +114,27 @@ async function updateAccount(account, change) {
|
|
|
111
114
|
if (jsonWas !== JSON.stringify(account)) // this test will miss the 'username' field, because hidden, but renameAccount is already calling saveAccountsASAP
|
|
112
115
|
saveAccountsAsap();
|
|
113
116
|
}
|
|
114
|
-
|
|
117
|
+
function reconcileAccountSessions() {
|
|
118
|
+
(0, sessionStamp_1.syncSessionStamps)(lodash_1.default.mapKeys(exports.accounts.get(), (_account, username) => normalizeUsername(username)));
|
|
119
|
+
}
|
|
120
|
+
function saveAccountsAsap() {
|
|
121
|
+
// API callers need the updated stamp before preserving their own session in the response
|
|
122
|
+
reconcileAccountSessions();
|
|
123
|
+
(0, config_1.saveConfigAsap)();
|
|
124
|
+
}
|
|
125
|
+
function invalidateAccountSessions(account) {
|
|
126
|
+
const found = typeof account === 'string' ? getAccount(account) : account;
|
|
127
|
+
if (!found)
|
|
128
|
+
return false;
|
|
129
|
+
(0, sessionStamp_1.renewSessionStamp)(found.username);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
115
132
|
exports.accounts = (0, config_1.defineConfig)(misc_1.CFG.accounts, {});
|
|
133
|
+
exports.accounts.sub(async () => {
|
|
134
|
+
await sessionStamp_1.sessionStampsReady;
|
|
135
|
+
// do not debounce: removing and recreating a username must not be coalesced into one change
|
|
136
|
+
reconcileAccountSessions();
|
|
137
|
+
});
|
|
116
138
|
exports.accounts.sub(lodash_1.default.debounce(obj => {
|
|
117
139
|
// consider some validation here, in case of manual edit of the config
|
|
118
140
|
lodash_1.default.each(obj, (rec, k) => {
|
|
@@ -190,14 +212,14 @@ function addAccount(username, props, updateExisting = false) {
|
|
|
190
212
|
if (account && !updateExisting)
|
|
191
213
|
return;
|
|
192
214
|
account = (0, misc_1.setHidden)(account || {}, { username }); // hidden so that stringification won't include it
|
|
193
|
-
Object.assign(account, lodash_1.default.pickBy(props, Boolean));
|
|
194
215
|
exports.accounts.set(was => Object.assign(was, { [username]: account }));
|
|
195
|
-
return updateAccount(account,
|
|
216
|
+
return updateAccount(account, lodash_1.default.pickBy(props, Boolean)).then(() => account);
|
|
196
217
|
}
|
|
197
218
|
function delAccount(username) {
|
|
198
219
|
if (!getAccount(username))
|
|
199
220
|
return false;
|
|
200
|
-
|
|
221
|
+
username = normalizeUsername(username);
|
|
222
|
+
exports.accounts.set(was => lodash_1.default.omit(was, username));
|
|
201
223
|
saveAccountsAsap();
|
|
202
224
|
return true;
|
|
203
225
|
}
|
package/src/plugins.js
CHANGED
|
@@ -298,8 +298,8 @@ const pluginsMiddleware = async (ctx, next) => {
|
|
|
298
298
|
const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
|
|
299
299
|
const name = a.shift();
|
|
300
300
|
if (plugins.has(name)) { // do it only if the plugin is loaded
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
// this server-owned route is GUI content; client headers must not grant the exemption
|
|
302
|
+
ctx.state.considerAsGui = true;
|
|
303
303
|
await (0, serveFile_1.serveFile)(ctx, plugins.get(name).folder + '/public/' + a.join('/'), const_1.MIME_AUTO);
|
|
304
304
|
}
|
|
305
305
|
return;
|
package/src/serveFile.js
CHANGED
|
@@ -31,10 +31,10 @@ const crypto_1 = require("crypto");
|
|
|
31
31
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
|
32
32
|
const upload_1 = require("./upload");
|
|
33
33
|
const allowedReferer = (0, config_1.defineConfig)(misc_1.CFG.allowed_referer, '');
|
|
34
|
+
const requireUploadApprovalForActiveContent = (0, config_1.defineConfig)(misc_1.CFG.require_upload_approval_for_active_content, true);
|
|
34
35
|
const maxDownloads = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads, 0), () => true);
|
|
35
36
|
const maxDownloadsPerIp = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_ip, 0), ctx => ctx.ip);
|
|
36
37
|
const maxDownloadsPerAccount = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_account, 0), ctx => (0, auth_1.getCurrentUsername)(ctx) || undefined);
|
|
37
|
-
const GUI_ASSET_MIME = /^(image\/|audio\/|video\/|font\/|text\/css$|(?:text|application)\/(?:javascript|ecmascript)$|application\/x-javascript$)/i;
|
|
38
38
|
function toAsciiEquivalent(s) {
|
|
39
39
|
return iconv_lite_1.default.encode(iconv_lite_1.default.decode(Buffer.from(s), 'utf-8'), 'ascii').toString().replaceAll('?', '');
|
|
40
40
|
}
|
|
@@ -64,15 +64,11 @@ async function serveFileNode(ctx, node) {
|
|
|
64
64
|
const download = 'dl' in ctx.query;
|
|
65
65
|
disposition(ctx, name, download);
|
|
66
66
|
if (!download && source && isActiveContentMime(effectiveMime)) {
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
if ((0, upload_1.isUploading)(source)
|
|
68
|
+
|| requireUploadApprovalForActiveContent.get() && (await (0, upload_1.getUploadMeta)(source))?.approved === false)
|
|
69
69
|
return (0, errorPages_1.sendErrorPage)(ctx, const_1.HTTP_FORBIDDEN);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
ctx.state.considerAsGui ??= !download && ctx.get('referer')?.endsWith('/')
|
|
73
|
-
&& (fetchDest ? fetchDest !== 'document' && fetchDest !== 'empty' // modern clients
|
|
74
|
-
// legacy clients often send Accept: */* for archive downloads, so the served mime is a safer signal than request headers here
|
|
75
|
-
: GUI_ASSET_MIME.test(effectiveMime));
|
|
71
|
+
// shared files count even when embedded: client headers cannot grant exemptions from limits or logging
|
|
76
72
|
await serveFile(ctx, source || '', effectiveMime);
|
|
77
73
|
await enforceDownloadLimits(ctx);
|
|
78
74
|
}
|
|
@@ -111,7 +107,7 @@ async function serveFile(ctx, filePath, mime, cached) {
|
|
|
111
107
|
ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
|
|
112
108
|
return;
|
|
113
109
|
}
|
|
114
|
-
if (ctx.
|
|
110
|
+
if (!ctx.state.getOrHead)
|
|
115
111
|
return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
|
|
116
112
|
try {
|
|
117
113
|
const stats = cached?.stats || await (0, util_1.promisify)(fs_1.stat)(filePath); // using fs's function instead of fs/promises, because only the former is supported by pkg
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.serveSharedFiles = exports.guiFilesMiddleware = void 0;
|
|
7
|
+
const deletePermission_1 = require("./deletePermission");
|
|
7
8
|
const path_1 = require("path");
|
|
8
9
|
const vfs_1 = require("./vfs");
|
|
9
10
|
const errorPages_1 = require("./errorPages");
|
|
@@ -95,7 +96,7 @@ const serveSharedFiles = async (ctx, next) => {
|
|
|
95
96
|
}
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
|
-
if (/^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get()
|
|
99
|
+
if (ctx.state.getOrHead && /^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get()) // originalUrl to not be subject to changes (vhosting plugin)
|
|
99
100
|
return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
|
|
100
101
|
let node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
101
102
|
if (!node)
|
|
@@ -106,9 +107,9 @@ const serveSharedFiles = async (ctx, next) => {
|
|
|
106
107
|
const { source } = node;
|
|
107
108
|
if (!source)
|
|
108
109
|
return ctx.status = cross_const_1.HTTP_METHOD_NOT_ALLOWED;
|
|
109
|
-
if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
|
|
110
|
-
return;
|
|
111
110
|
try {
|
|
111
|
+
if (!await (0, deletePermission_1.checkDeletePermission)(node, ctx))
|
|
112
|
+
return;
|
|
112
113
|
if ((await events_1.default.emitAsync('deleting', { node, ctx }))?.isDefaultPrevented())
|
|
113
114
|
return ctx.status = cross_const_1.HTTP_FAILED_DEPENDENCY;
|
|
114
115
|
await (0, promises_2.rm)(source, { recursive: true });
|
|
@@ -122,11 +123,11 @@ const serveSharedFiles = async (ctx, next) => {
|
|
|
122
123
|
return ctx.status = cross_const_1.HTTP_SERVER_ERROR;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
|
-
if (path.endsWith('/') && !get)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
if (path.endsWith('/') && !get) // final slash needed on browsers to make resource urls working with html pages
|
|
127
|
+
node = await (0, vfs_1.getDefaultFile)(node, ctx) || node;
|
|
128
|
+
// classify the resolved default so a file can override its folder's GUI status
|
|
129
|
+
if (node.gui_asset)
|
|
130
|
+
ctx.state.considerAsGui = true;
|
|
130
131
|
if (get === 'icon')
|
|
131
132
|
return (0, serveFile_1.serveFile)(ctx, node.icon || '|'); // pipe to cause not-found
|
|
132
133
|
if (!(0, vfs_1.nodeIsFolder)(node))
|