├── imgs
├── 1.png
├── 2.png
└── 3.png
├── themes
├── logo.png
├── classic
│ └── app.js
└── material
│ └── app.js
├── README.md
├── README_zh.md
└── index.js
/imgs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanzai/goindex/HEAD/imgs/1.png
--------------------------------------------------------------------------------
/imgs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanzai/goindex/HEAD/imgs/2.png
--------------------------------------------------------------------------------
/imgs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanzai/goindex/HEAD/imgs/3.png
--------------------------------------------------------------------------------
/themes/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yanzai/goindex/HEAD/themes/logo.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | GoIndex
4 | ====
5 |
6 | 这是一个 [修改版的goindex](https://github.com/yanzai/goindex) ,在 [原版goindex](https://github.com/donwa/goindex) 基础上添加了多盘支持、搜索、分页加载等功能。
7 |
8 | `index.js` 包含 Workers 所需的代码.
9 |
10 | ## 预览
11 |
12 | Demo: https://yanzai-goindex.java.workers.dev
13 |
14 |
15 |
16 | 多盘:
17 | 
18 |
19 |
20 |
21 | 搜索:
22 | 
23 |
24 |
25 |
26 | 分页:
27 | 
28 |
29 |
30 |
31 | ## 更新日志
32 |
33 | ### 2020-4-28
34 |
35 | - 添加 Basic Auth 认证,每个盘符可单独配置用户名和密码,可以保护该盘下所有子文件和子文件夹
36 |
37 | - 支持自定义 web 界面主题色,添加了 dark_mode ; 在 `uiConfig` 中可以配置
38 |
39 | - 原 goindex 的 .password 验证方式作为后备验证方式得以保留,但默认不开启
40 |
41 | 以上,详见 `index.js` 中的配置项的注释。
42 |
43 | ### 2020-4-23
44 |
45 | - 支持调用 nPlayer / MXPlayer Free / MXPlayer Pro / PotPlayer / VLC 播放,支持直接复制直链
46 | - 简单支持 PDF 文件预览
47 | - 可以配置是否允许其他 web 前端 cors 方式获取文件
48 |
49 | ### 2020-3-9
50 |
51 | - flac file play support
52 |
53 | ### 2020-3-7
54 |
55 | - 添加搜索功能,搜索结果分页增量展示,并支持跳转到对应路径浏览
56 | - 搜索功能支持个人盘和团队盘全盘搜索
57 | - 搜索分页大小可配置,具体见 `index.js` 注释
58 | - 尝试解决移动端滚动到底部时的增量加载问题
59 | - UI优化,盘符选择改为下拉框展示
60 |
61 | ### 2020-3-5
62 |
63 | - 文件列表页分页增量加载,支持自定义分页大小,多页内容的可以缓存,配置见 `index.js` 注释
64 | - 图片浏览页 下一张/上一张 导航
65 | - 优化列目录时的速度
66 |
67 | ### 2020-3-4
68 |
69 | 在原版基础上修改:
70 |
71 | - 添加多盘支持,自主设置要显示的多盘及各自密码
72 | - 前端只修改了 material ,故不支持 classic 主题
73 | - 配置见 `index.js` 注释
74 |
75 |
76 | ---
77 |
78 |
79 |
80 | > **安装部署可以参考原版,以下摘自原版 goindex 的部署说明:**
81 |
82 |
83 |
84 | ## Demo
85 | material: [https://index.gd.workers.dev/](https://index.gd.workers.dev/)
86 | classic: [https://indexc.gd.workers.dev/](https://indexc.gd.workers.dev/)
87 |
88 | ## Deployment
89 | 1.Install `rclone` software locally
90 | 2.Follow [https://rclone.org/drive/]( https://rclone.org/drive/) bind a drive
91 | 3.Execute the command`rclone config file` to find the file `rclone.conf` path
92 | 4.Open `rclone.conf`,find the configuration `root_folder_id` and `refresh_token`
93 | 5.Download index.js in https://github.com/donwa/goindex and fill in root and refresh_token
94 | 6.Deploy the code to [Cloudflare Workers](https://www.cloudflare.com/)
95 |
96 | ## Quick Deployment
97 | 1.Open https://installen.gd.workers.dev/
98 | 2.Auth and get the code
99 | 3.Deploy the code to [Cloudflare Workers](https://www.cloudflare.com/)
100 |
101 |
102 |
103 | ## About
104 | Cloudflare Workers allow you to write JavaScript which runs on all of Cloudflare's 150+ global data centers.
105 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | GoIndex
4 | ====
5 |
6 | 这是一个 [修改版的goindex](https://github.com/yanzai/goindex) ,在 [原版goindex](https://github.com/donwa/goindex) 基础上添加了多盘支持、搜索、分页加载等功能。
7 |
8 | `index.js` 包含 Workers 所需的代码.
9 |
10 | ## 预览
11 |
12 | Demo: https://yanzai-goindex.java.workers.dev
13 |
14 |
15 |
16 | 多盘:
17 | 
18 |
19 |
20 |
21 | 搜索:
22 | 
23 |
24 |
25 |
26 | 分页:
27 | 
28 |
29 |
30 |
31 | ## 更新日志
32 |
33 | ### 2020-4-28
34 |
35 | - 添加 Basic Auth 认证,每个盘符可单独配置用户名和密码,可以保护该盘下所有子文件和子文件夹
36 |
37 | - 支持自定义 web 界面主题色,添加了 dark_mode ; 在 `uiConfig` 中可以配置
38 |
39 | - 原 goindex 的 .password 验证方式作为后备验证方式得以保留,但默认不开启
40 |
41 | 以上,详见 `index.js` 中的配置项的注释。
42 |
43 | ### 2020-4-23
44 |
45 | - 支持调用 nPlayer / MXPlayer Free / MXPlayer Pro / PotPlayer / VLC 播放,支持直接复制直链
46 | - 简单支持 PDF 文件预览
47 | - 可以配置是否允许其他 web 前端 cors 方式获取文件
48 |
49 | ### 2020-3-9
50 |
51 | - flac file play support
52 |
53 | ### 2020-3-7
54 |
55 | - 添加搜索功能,搜索结果分页增量展示,并支持跳转到对应路径浏览
56 | - 搜索功能支持个人盘和团队盘全盘搜索
57 | - 搜索分页大小可配置,具体见 `index.js` 注释
58 | - 尝试解决移动端滚动到底部时的增量加载问题
59 | - UI优化,盘符选择改为下拉框展示
60 |
61 | ### 2020-3-5
62 |
63 | - 文件列表页分页增量加载,支持自定义分页大小,多页内容的可以缓存,配置见 `index.js` 注释
64 | - 图片浏览页 下一张/上一张 导航
65 | - 优化列目录时的速度
66 |
67 | ### 2020-3-4
68 |
69 | 在原版基础上修改:
70 |
71 | - 添加多盘支持,自主设置要显示的多盘及各自密码
72 | - 前端只修改了 material ,故不支持 classic 主题
73 | - 配置见 `index.js` 注释
74 |
75 |
76 | ---
77 |
78 |
79 |
80 | > **安装部署可以参考原版,以下摘自原版 goindex 的部署说明:**
81 |
82 | ## Demo
83 | material:
84 | [https://index.gd.workers.dev/](https://index.gd.workers.dev/)
85 | classic:
86 | [https://indexc.gd.workers.dev/](https://indexc.gd.workers.dev/)
87 |
88 | ## 安装部署方案1
89 | 1、在本地安装 rclone
90 | 2、按照 https://rclone.org/drive/ 流程进行授权。
91 | 3、执行 rclone config file 查看 rclone.conf 路径。找到root_folder_id和refresh_token记录下来。
92 | 4、下载 https://github.com/donwa/goindex 中的 index.js 并填入 root 和 refresh_token
93 | 5、复制代码 到 CloudFlare 部署。
94 |
95 | ## 安装部署方案2
96 | 作者不会记录refresh_token,但为避免纠纷,建议有条件的同学使用方案1进行部署
97 | 1、访问[https://install.gd.workers.dev/](https://install.gd.workers.dev/)
98 | 2、授权认证后,生成部署代码。
99 | 3、复制代码 到 CloudFlare 部署。
100 |
101 | ## 文件夹密码:
102 | 在google drive 文件中放置 `.password` 文件来设置密码。
103 | 密码文件只能保护该文件不被列举,不能保护该文件夹的子文件夹不被列举。
104 | 也不保护文件夹下文件不被下载。
105 |
106 | 程序文件中 `root_pass` 只为根目录密码,优先于 `.password` 文件
107 |
108 |
109 | ## 更新日志
110 |
111 | 1.0.6
112 | 添加 classic 模板
113 |
114 | 1.0.5
115 | 添加文件展示页
116 |
117 | 1.0.4
118 | 修复 注入问题。
119 |
120 | 1.0.3
121 | 修复 `.password` 绕过下载问题。
122 |
123 | 1.0.2
124 | 优化前端逻辑
125 | 添加文件预览功能(临时)
126 | 添加前端文件缓存功能
127 |
128 | 1.0.1
129 | 添加 README.md 、 HEAD.md 支持
130 |
131 | 1.0.0
132 | 前后端分离,确定基本架构
133 | 添加.password 支持
134 |
--------------------------------------------------------------------------------
/themes/classic/app.js:
--------------------------------------------------------------------------------
1 | document.write('');
2 | // 初始化页面,并载入必要资源
3 | function init(){
4 | document.siteName = $('title').html();
5 | $('body').addClass("mdui-theme-primary-blue-grey mdui-theme-accent-blue");
6 | var html = `
7 |
Index of
8 |
10 | `;
11 | $('body').html(html);
12 | }
13 |
14 | function render(path){
15 | if(path.indexOf("?") > 0){
16 | path = path.substr(0,path.indexOf("?"));
17 | }
18 | title(path);
19 | nav(path);
20 | if(path.substr(-1) == '/'){
21 | list(path);
22 | }else{
23 | file(path);
24 | }
25 | }
26 |
27 |
28 | // 渲染 title
29 | function title(path){
30 | path = decodeURI(path);
31 | $('title').html(document.siteName+' - '+path);
32 | }
33 |
34 | // 渲染导航栏
35 | function nav(path){
36 | path = decodeURI(path);
37 | $('#heading').html('Index of '+path);
38 | }
39 |
40 | // 渲染文件列表
41 | function list(path){
42 | var content = `
43 | | Name | Size | Date Modified |
44 | `;
45 |
46 | if(path != '/'){
47 | var up = path.split('/');
48 | up.pop();up.pop();
49 | up = up.join('/')+'/';
50 | content += `
51 |
52 | |
53 | ..
54 | |
55 | |
56 | |
57 |
58 | `;
59 | }
60 | $('#table').html(content);
61 |
62 | var password = localStorage.getItem('password'+path);
63 | $.post(path,'{"password":"'+password+'"}', function(data,status){
64 | var obj = jQuery.parseJSON(data);
65 | if(typeof obj != 'null' && obj.hasOwnProperty('error') && obj.error.code == '401'){
66 | var pass = prompt("password","");
67 | localStorage.setItem('password'+path, pass);
68 | if(pass != null && pass != ""){
69 | list(path);
70 | }else{
71 | history.go(-1);
72 | }
73 | }else if(typeof obj != 'null'){
74 | list_files(path,obj.files);
75 | }
76 | });
77 | }
78 |
79 | function list_files(path,files){
80 | html = "";
81 | for(i in files){
82 | var item = files[i];
83 | if(item['size']==undefined){
84 | item['size'] = "";
85 | }
86 | item['modifiedTime'] = utc2beijing(item['modifiedTime']);
87 | item['size'] = formatFileSize(item['size']);
88 | if(item['mimeType'] == 'application/vnd.google-apps.folder'){
89 | var p = path+item.name+'/';
90 | html +=`
91 |
92 | | ${item.name}/ |
93 | ${item['size']} |
94 | ${item['modifiedTime']} |
95 |
96 | `;
97 | }else{
98 | var p = path+item.name;
99 | html += `
100 |
101 | | ${item.name} |
102 | ${item['size']} |
103 | ${item['modifiedTime']} |
104 |
105 | `;
106 | }
107 | }
108 | $('#table').append(html);
109 | }
110 |
111 | //时间转换
112 | function utc2beijing(utc_datetime) {
113 | // 转为正常的时间格式 年-月-日 时:分:秒
114 | var T_pos = utc_datetime.indexOf('T');
115 | var Z_pos = utc_datetime.indexOf('Z');
116 | var year_month_day = utc_datetime.substr(0,T_pos);
117 | var hour_minute_second = utc_datetime.substr(T_pos+1,Z_pos-T_pos-1);
118 | var new_datetime = year_month_day+" "+hour_minute_second; // 2017-03-31 08:02:06
119 |
120 | // 处理成为时间戳
121 | timestamp = new Date(Date.parse(new_datetime));
122 | timestamp = timestamp.getTime();
123 | timestamp = timestamp/1000;
124 |
125 | // 增加8个小时,北京时间比utc时间多八个时区
126 | var unixtimestamp = timestamp+8*60*60;
127 |
128 | // 时间戳转为时间
129 | var unixtimestamp = new Date(unixtimestamp*1000);
130 | var year = 1900 + unixtimestamp.getYear();
131 | var month = "0" + (unixtimestamp.getMonth() + 1);
132 | var date = "0" + unixtimestamp.getDate();
133 | var hour = "0" + unixtimestamp.getHours();
134 | var minute = "0" + unixtimestamp.getMinutes();
135 | var second = "0" + unixtimestamp.getSeconds();
136 | return year + "-" + month.substring(month.length-2, month.length) + "-" + date.substring(date.length-2, date.length)
137 | + " " + hour.substring(hour.length-2, hour.length) + ":"
138 | + minute.substring(minute.length-2, minute.length) + ":"
139 | + second.substring(second.length-2, second.length);
140 | }
141 |
142 | // bytes自适应转换到KB,MB,GB
143 | function formatFileSize(bytes) {
144 | if (bytes>=1000000000) {bytes=(bytes/1000000000).toFixed(2)+' GB';}
145 | else if (bytes>=1000000) {bytes=(bytes/1000000).toFixed(2)+' MB';}
146 | else if (bytes>=1000) {bytes=(bytes/1000).toFixed(2)+' KB';}
147 | else if (bytes>1) {bytes=bytes+' bytes';}
148 | else if (bytes==1) {bytes=bytes+' byte';}
149 | else {bytes='';}
150 | return bytes;
151 | }
152 |
153 | // 监听回退事件
154 | window.onpopstate = function(){
155 | var path = window.location.pathname;
156 | render(path);
157 | }
158 |
159 |
160 | $(function(){
161 | init();
162 | var path = window.location.pathname;
163 | $("body").on("click",'.folder',function(){
164 | var url = $(this).attr('href');
165 | history.pushState(null, null, url);
166 | render(url);
167 | return false;
168 | });
169 |
170 | render(path);
171 | });
172 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const authConfig = {
2 | "siteName": "GoIndex", // 网站名称
3 | "version": "_4.28", // 程序版本。用户不要手动修改
4 | /*"client_id": "202264815644.apps.googleusercontent.com",
5 | "client_secret": "X4Z3ca8xfWDb1Voo-F9a7ZxJ",*/
6 | // 【注意】强烈推荐使用自己的 client_id 和 client_secret
7 | "client_id": "",
8 | "client_secret": "",
9 | "refresh_token": "", // 授权 token
10 | /**
11 | * 设置要显示的多个云端硬盘;按格式添加多个
12 | * [id]: 可以是 团队盘id、子文件夹id、或者"root"(代表个人盘根目录);
13 | * [name]: 显示的名称
14 | * [user]: Basic Auth 的用户名
15 | * [pass]: Basic Auth 的密码
16 | * [protect_file_link]: Basic Auth 是否用于保护文件链接,默认值(不设置时)为 false,即不保护文件链接(方便 直链下载/外部播放 等)
17 | * 每个盘的 Basic Auth 都可以单独设置。Basic Auth 默认保护该盘下所有文件夹/子文件夹路径
18 | * 【注意】默认不保护文件链接,这样可以方便 直链下载/外部播放;
19 | * 如果要保护文件链接,需要将 protect_file_link 设置为 true,此时如果要进行外部播放等操作,需要将 host 替换为 user:pass@host 的 形式
20 | * 不需要 Basic Auth 的盘,保持 user 和 pass 同时为空即可。(直接不设置也可以)
21 | * 【注意】对于id设置为为子文件夹id的盘将不支持搜索功能(不影响其他盘)。
22 | */
23 | "roots": [
24 | {
25 | id: "root",
26 | name: "个人盘"
27 | },
28 | {
29 | id: "drive_id",
30 | name: "团队盘1",
31 | user: 'user1',
32 | pass: "111",
33 | protect_file_link: true
34 | },
35 | {
36 | id: "folder_id",
37 | name: "文件夹",
38 | // 只设置密码、只设置用户名、同时设置用户名密码,都是可以的
39 | user: '',
40 | pass: "222",
41 | protect_file_link: false
42 | }
43 | ],
44 | /**
45 | * 文件列表页面每页显示的数量。【推荐设置值为 100 到 1000 之间】;
46 | * 如果设置大于1000,会导致请求 drive api 时出错;
47 | * 如果设置的值过小,会导致文件列表页面滚动条增量加载(分页加载)失效;
48 | * 此值的另一个作用是,如果目录内文件数大于此设置值(即需要多页展示的),将会对首次列目录结果进行缓存。
49 | */
50 | "files_list_page_size": 500,
51 | /**
52 | * 搜索结果页面每页显示的数量。【推荐设置值为 50 到 1000 之间】;
53 | * 如果设置大于1000,会导致请求 drive api 时出错;
54 | * 如果设置的值过小,会导致搜索结果页面滚动条增量加载(分页加载)失效;
55 | * 此值的大小影响搜索操作的响应速度。
56 | */
57 | "search_result_list_page_size": 50,
58 | // 确认有 cors 用途的可以开启
59 | "enable_cors_file_down": false,
60 | /**
61 | * 上面的 basic auth 已经包含了盘内全局保护的功能。所以默认不再去认证 .password 文件内的密码;
62 | * 如果在全局认证的基础上,仍需要给某些目录单独进行 .password 文件内的密码验证的话,将此选项设置为 true;
63 | * 【注意】如果开启了 .password 文件密码验证,每次列目录都会额外增加查询目录内 .password 文件是否存在的开销。
64 | */
65 | "enable_password_file_verify": false
66 | };
67 |
68 | /**
69 | * web ui 设置
70 | */
71 | const uiConfig = {
72 | // 此版本只支持 material
73 | "theme": "material", // DO NOT set it to classic
74 | "dark_mode": false,
75 | "main_color": "blue-grey",
76 | "accent_color": "blue",
77 | /*"main_color": "light-green",
78 | "accent_color": "green",*/
79 | "fluid_navigation_bar": true,
80 | };
81 |
82 | /**
83 | * global functions
84 | */
85 | const FUNCS = {
86 | /**
87 | * 转换成针对谷歌搜索词法相对安全的搜索关键词
88 | */
89 | formatSearchKeyword: function (keyword) {
90 | let nothing = "";
91 | let space = " ";
92 | if (!keyword) return nothing;
93 | return keyword.replace(/(!=)|['"=<>/\\:]/g, nothing)
94 | .replace(/[,,|(){}]/g, space)
95 | .trim()
96 | }
97 |
98 | };
99 |
100 | /**
101 | * global consts
102 | * @type {{folder_mime_type: string, default_file_fields: string, gd_root_type: {share_drive: number, user_drive: number, sub_folder: number}}}
103 | */
104 | const CONSTS = new (class {
105 | default_file_fields = 'parents,id,name,mimeType,modifiedTime,createdTime,fileExtension,size';
106 | gd_root_type = {
107 | user_drive: 0,
108 | share_drive: 1,
109 | sub_folder: 2
110 | };
111 | folder_mime_type = 'application/vnd.google-apps.folder';
112 | })();
113 |
114 |
115 | // gd instances
116 | var gds = [];
117 |
118 | function html(current_drive_order = 0, model = {}) {
119 | return `
120 |
121 |
122 |
123 |
124 |
125 | ${authConfig.siteName}
126 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | `;
139 | };
140 |
141 | addEventListener('fetch', event => {
142 | event.respondWith(handleRequest(event.request));
143 | });
144 |
145 | /**
146 | * Fetch and log a request
147 | * @param {Request} request
148 | */
149 | async function handleRequest(request) {
150 | if (gds.length === 0) {
151 | for (let i = 0; i < authConfig.roots.length; i++) {
152 | const gd = new googleDrive(authConfig, i);
153 | await gd.init();
154 | gds.push(gd)
155 | }
156 | // 这个操作并行,提高效率
157 | let tasks = [];
158 | gds.forEach(gd => {
159 | tasks.push(gd.initRootType());
160 | });
161 | for (let task of tasks) {
162 | await task;
163 | }
164 | }
165 |
166 | // 从 path 中提取 drive order
167 | // 并根据 drive order 获取对应的 gd instance
168 | let gd;
169 | let url = new URL(request.url);
170 | let path = url.pathname;
171 |
172 | /**
173 | * 重定向至起始页
174 | * @returns {Response}
175 | */
176 | function redirectToIndexPage() {
177 | return new Response('', {status: 301, headers: {'Location': `${url.origin}/0:/`}});
178 | }
179 |
180 | if (path == '/') return redirectToIndexPage();
181 | if (path.toLowerCase() == '/favicon.ico') {
182 | // 后面可以找一个 favicon
183 | return new Response('', {status: 404})
184 | }
185 |
186 | // 特殊命令格式
187 | const command_reg = /^\/(?\d+):(?[a-zA-Z0-9]+)$/g;
188 | const match = command_reg.exec(path);
189 | if (match) {
190 | const num = match.groups.num;
191 | const order = Number(num);
192 | if (order >= 0 && order < gds.length) {
193 | gd = gds[order];
194 | } else {
195 | return redirectToIndexPage()
196 | }
197 | // basic auth
198 | for (const r = gd.basicAuthResponse(request); r;) return r;
199 | const command = match.groups.command;
200 | // 搜索
201 | if (command === 'search') {
202 | if (request.method === 'POST') {
203 | // 搜索结果
204 | return handleSearch(request, gd);
205 | } else {
206 | const params = url.searchParams;
207 | // 搜索页面
208 | return new Response(html(gd.order, {
209 | q: params.get("q") || '',
210 | is_search_page: true,
211 | root_type: gd.root_type
212 | }),
213 | {
214 | status: 200,
215 | headers: {'Content-Type': 'text/html; charset=utf-8'}
216 | });
217 | }
218 | } else if (command === 'id2path' && request.method === 'POST') {
219 | return handleId2Path(request, gd)
220 | }
221 | }
222 |
223 | // 期望的 path 格式
224 | const common_reg = /^\/\d+:\/.*$/g;
225 | try {
226 | if (!path.match(common_reg)) {
227 | return redirectToIndexPage();
228 | }
229 | let split = path.split("/");
230 | let order = Number(split[1].slice(0, -1));
231 | if (order >= 0 && order < gds.length) {
232 | gd = gds[order];
233 | } else {
234 | return redirectToIndexPage()
235 | }
236 | } catch (e) {
237 | return redirectToIndexPage()
238 | }
239 |
240 | // basic auth
241 | // for (const r = gd.basicAuthResponse(request); r;) return r;
242 | const basic_auth_res = gd.basicAuthResponse(request);
243 |
244 | path = path.replace(gd.url_path_prefix, '') || '/';
245 | if (request.method == 'POST') {
246 | return basic_auth_res || apiRequest(request, gd);
247 | }
248 |
249 | let action = url.searchParams.get('a');
250 |
251 | if (path.substr(-1) == '/' || action != null) {
252 | return basic_auth_res || new Response(html(gd.order, {root_type: gd.root_type}), {
253 | status: 200,
254 | headers: {'Content-Type': 'text/html; charset=utf-8'}
255 | });
256 | } else {
257 | if (path.split('/').pop().toLowerCase() == ".password") {
258 | return basic_auth_res || new Response("", {status: 404});
259 | }
260 | let file = await gd.file(path);
261 | let range = request.headers.get('Range');
262 | const inline_down = 'true' === url.searchParams.get('inline');
263 | if (gd.root.protect_file_link && basic_auth_res) return basic_auth_res;
264 | return gd.down(file.id, range, inline_down);
265 | }
266 | }
267 |
268 |
269 | async function apiRequest(request, gd) {
270 | let url = new URL(request.url);
271 | let path = url.pathname;
272 | path = path.replace(gd.url_path_prefix, '') || '/';
273 |
274 | let option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}
275 |
276 | if (path.substr(-1) == '/') {
277 | let form = await request.formData();
278 | // 这样可以提升首次列目录时的速度。缺点是,如果password验证失败,也依然会产生列目录的开销
279 | let deferred_list_result = gd.list(path, form.get('page_token'), Number(form.get('page_index')));
280 |
281 | // check .password file, if `enable_password_file_verify` is true
282 | if (authConfig['enable_password_file_verify']) {
283 | let password = await gd.password(path);
284 | // console.log("dir password", password);
285 | if (password && password.replace("\n", "") !== form.get('password')) {
286 | let html = `{"error": {"code": 401,"message": "password error."}}`;
287 | return new Response(html, option);
288 | }
289 | }
290 |
291 | let list_result = await deferred_list_result;
292 | return new Response(JSON.stringify(list_result), option);
293 | } else {
294 | let file = await gd.file(path);
295 | let range = request.headers.get('Range');
296 | return new Response(JSON.stringify(file));
297 | }
298 | }
299 |
300 | // 处理 search
301 | async function handleSearch(request, gd) {
302 | const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}};
303 | let form = await request.formData();
304 | let search_result = await
305 | gd.search(form.get('q') || '', form.get('page_token'), Number(form.get('page_index')));
306 | return new Response(JSON.stringify(search_result), option);
307 | }
308 |
309 | /**
310 | * 处理 id2path
311 | * @param request 需要 id 参数
312 | * @param gd
313 | * @returns {Promise} 【注意】如果从前台接收的id代表的项目不在目标gd盘下,那么response会返回给前台一个空字符串""
314 | */
315 | async function handleId2Path(request, gd) {
316 | const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}};
317 | let form = await request.formData();
318 | let path = await gd.findPathById(form.get('id'));
319 | return new Response(path || '', option);
320 | }
321 |
322 | class googleDrive {
323 | constructor(authConfig, order) {
324 | // 每个盘对应一个order,对应一个gd实例
325 | this.order = order;
326 | this.root = authConfig.roots[order];
327 | this.root.protect_file_link = this.root.protect_file_link || false;
328 | this.url_path_prefix = `/${order}:`;
329 | this.authConfig = authConfig;
330 | // TODO: 这些缓存的失效刷新策略,后期可以制定一下
331 | // path id
332 | this.paths = [];
333 | // path file
334 | this.files = [];
335 | // path pass
336 | this.passwords = [];
337 | // id <-> path
338 | this.id_path_cache = {};
339 | this.id_path_cache[this.root['id']] = '/';
340 | this.paths["/"] = this.root['id'];
341 | /*if (this.root['pass'] != "") {
342 | this.passwords['/'] = this.root['pass'];
343 | }*/
344 | // this.init();
345 | }
346 |
347 | /**
348 | * 初次授权;然后获取 user_drive_real_root_id
349 | * @returns {Promise}
350 | */
351 | async init() {
352 | await this.accessToken();
353 | /*await (async () => {
354 | // 只获取1次
355 | if (authConfig.user_drive_real_root_id) return;
356 | const root_obj = await (gds[0] || this).findItemById('root');
357 | if (root_obj && root_obj.id) {
358 | authConfig.user_drive_real_root_id = root_obj.id
359 | }
360 | })();*/
361 | // 等待 user_drive_real_root_id ,只获取1次
362 | if (authConfig.user_drive_real_root_id) return;
363 | const root_obj = await (gds[0] || this).findItemById('root');
364 | if (root_obj && root_obj.id) {
365 | authConfig.user_drive_real_root_id = root_obj.id
366 | }
367 | }
368 |
369 | /**
370 | * 获取根目录类型,设置到 root_type
371 | * @returns {Promise}
372 | */
373 | async initRootType() {
374 | const root_id = this.root['id'];
375 | const types = CONSTS.gd_root_type;
376 | if (root_id === 'root' || root_id === authConfig.user_drive_real_root_id) {
377 | this.root_type = types.user_drive;
378 | } else {
379 | const obj = await this.getShareDriveObjById(root_id);
380 | this.root_type = obj ? types.share_drive : types.sub_folder;
381 | }
382 | }
383 |
384 | /**
385 | * Returns a response that requires authorization, or null
386 | * @param request
387 | * @returns {Response|null}
388 | */
389 | basicAuthResponse(request) {
390 | const user = this.root.user || '',
391 | pass = this.root.pass || '',
392 | _401 = new Response('Unauthorized', {
393 | headers: {'WWW-Authenticate': `Basic realm="goindex:drive:${this.order}"`},
394 | status: 401
395 | });
396 | if (user || pass) {
397 | const auth = request.headers.get('Authorization')
398 | if (auth) {
399 | try {
400 | const [received_user, received_pass] = atob(auth.split(' ').pop()).split(':');
401 | return (received_user === user && received_pass === pass) ? null : _401;
402 | } catch (e) {
403 | }
404 | }
405 | } else return null;
406 | return _401;
407 | }
408 |
409 | async down(id, range = '', inline = false) {
410 | let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`;
411 | let requestOption = await this.requestOption();
412 | requestOption.headers['Range'] = range;
413 | let res = await fetch(url, requestOption);
414 | const {headers} = res = new Response(res.body, res)
415 | this.authConfig.enable_cors_file_down && headers.append('Access-Control-Allow-Origin', '*');
416 | inline === true && headers.set('Content-Disposition', 'inline');
417 | return res;
418 | }
419 |
420 | async file(path) {
421 | if (typeof this.files[path] == 'undefined') {
422 | this.files[path] = await this._file(path);
423 | }
424 | return this.files[path];
425 | }
426 |
427 | async _file(path) {
428 | let arr = path.split('/');
429 | let name = arr.pop();
430 | name = decodeURIComponent(name).replace(/\'/g, "\\'");
431 | let dir = arr.join('/') + '/';
432 | // console.log(name, dir);
433 | let parent = await this.findPathId(dir);
434 | // console.log(parent);
435 | let url = 'https://www.googleapis.com/drive/v3/files';
436 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true};
437 | params.q = `'${parent}' in parents and name = '${name}' and trashed = false`;
438 | params.fields = "files(id, name, mimeType, size ,createdTime, modifiedTime, iconLink, thumbnailLink)";
439 | url += '?' + this.enQuery(params);
440 | let requestOption = await this.requestOption();
441 | let response = await fetch(url, requestOption);
442 | let obj = await response.json();
443 | // console.log(obj);
444 | return obj.files[0];
445 | }
446 |
447 | // 通过reqeust cache 来缓存
448 | async list(path, page_token = null, page_index = 0) {
449 | if (this.path_children_cache == undefined) {
450 | // { :[ {nextPageToken:'',data:{}}, {nextPageToken:'',data:{}} ...], ...}
451 | this.path_children_cache = {};
452 | }
453 |
454 | if (this.path_children_cache[path]
455 | && this.path_children_cache[path][page_index]
456 | && this.path_children_cache[path][page_index].data
457 | ) {
458 | let child_obj = this.path_children_cache[path][page_index];
459 | return {
460 | nextPageToken: child_obj.nextPageToken || null,
461 | curPageIndex: page_index,
462 | data: child_obj.data
463 | };
464 | }
465 |
466 | let id = await this.findPathId(path);
467 | let result = await this._ls(id, page_token, page_index);
468 | let data = result.data;
469 | // 对有多页的,进行缓存
470 | if (result.nextPageToken && data.files) {
471 | if (!Array.isArray(this.path_children_cache[path])) {
472 | this.path_children_cache[path] = []
473 | }
474 | this.path_children_cache[path][Number(result.curPageIndex)] = {
475 | nextPageToken: result.nextPageToken,
476 | data: data
477 | };
478 | }
479 |
480 | return result
481 | }
482 |
483 |
484 | async _ls(parent, page_token = null, page_index = 0) {
485 | // console.log("_ls", parent);
486 |
487 | if (parent == undefined) {
488 | return null;
489 | }
490 | let obj;
491 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true};
492 | params.q = `'${parent}' in parents and trashed = false AND name !='.password'`;
493 | params.orderBy = 'folder,name,modifiedTime desc';
494 | params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime)";
495 | params.pageSize = this.authConfig.files_list_page_size;
496 |
497 | if (page_token) {
498 | params.pageToken = page_token;
499 | }
500 | let url = 'https://www.googleapis.com/drive/v3/files';
501 | url += '?' + this.enQuery(params);
502 | let requestOption = await this.requestOption();
503 | let response = await fetch(url, requestOption);
504 | obj = await response.json();
505 |
506 | return {
507 | nextPageToken: obj.nextPageToken || null,
508 | curPageIndex: page_index,
509 | data: obj
510 | };
511 |
512 | /*do {
513 | if (pageToken) {
514 | params.pageToken = pageToken;
515 | }
516 | let url = 'https://www.googleapis.com/drive/v3/files';
517 | url += '?' + this.enQuery(params);
518 | let requestOption = await this.requestOption();
519 | let response = await fetch(url, requestOption);
520 | obj = await response.json();
521 | files.push(...obj.files);
522 | pageToken = obj.nextPageToken;
523 | } while (pageToken);*/
524 |
525 | }
526 |
527 | async password(path) {
528 | if (this.passwords[path] !== undefined) {
529 | return this.passwords[path];
530 | }
531 |
532 | // console.log("load", path, ".password", this.passwords[path]);
533 |
534 | let file = await this.file(path + '.password');
535 | if (file == undefined) {
536 | this.passwords[path] = null;
537 | } else {
538 | let url = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`;
539 | let requestOption = await this.requestOption();
540 | let response = await this.fetch200(url, requestOption);
541 | this.passwords[path] = await response.text();
542 | }
543 |
544 | return this.passwords[path];
545 | }
546 |
547 |
548 | /**
549 | * 通过 id 获取 share drive 信息
550 | * @param any_id
551 | * @returns {Promise} 任何非正常情况都返回 null
552 | */
553 | async getShareDriveObjById(any_id) {
554 | if (!any_id) return null;
555 | if ('string' !== typeof any_id) return null;
556 |
557 | let url = `https://www.googleapis.com/drive/v3/drives/${any_id}`;
558 | let requestOption = await this.requestOption();
559 | let res = await fetch(url, requestOption);
560 | let obj = await res.json();
561 | if (obj && obj.id) return obj;
562 |
563 | return null
564 | }
565 |
566 |
567 | /**
568 | * 搜索
569 | * @returns {Promise<{data: null, nextPageToken: null, curPageIndex: number}>}
570 | */
571 | async search(origin_keyword, page_token = null, page_index = 0) {
572 | const types = CONSTS.gd_root_type;
573 | const is_user_drive = this.root_type === types.user_drive;
574 | const is_share_drive = this.root_type === types.share_drive;
575 |
576 | const empty_result = {
577 | nextPageToken: null,
578 | curPageIndex: page_index,
579 | data: null
580 | };
581 |
582 | if (!is_user_drive && !is_share_drive) {
583 | return empty_result;
584 | }
585 | let keyword = FUNCS.formatSearchKeyword(origin_keyword);
586 | if (!keyword) {
587 | // 关键词为空,返回
588 | return empty_result;
589 | }
590 | let words = keyword.split(/\s+/);
591 | let name_search_str = `name contains '${words.join("' AND name contains '")}'`;
592 |
593 | // corpora 为 user 是个人盘 ,为 drive 是团队盘。配合 driveId
594 | let params = {};
595 | if (is_user_drive) {
596 | params.corpora = 'user'
597 | }
598 | if (is_share_drive) {
599 | params.corpora = 'drive';
600 | params.driveId = this.root.id;
601 | // This parameter will only be effective until June 1, 2020. Afterwards shared drive items will be included in the results.
602 | params.includeItemsFromAllDrives = true;
603 | params.supportsAllDrives = true;
604 | }
605 | if (page_token) {
606 | params.pageToken = page_token;
607 | }
608 | params.q = `trashed = false AND name !='.password' AND (${name_search_str})`;
609 | params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime)";
610 | params.pageSize = this.authConfig.search_result_list_page_size;
611 | // params.orderBy = 'folder,name,modifiedTime desc';
612 |
613 | let url = 'https://www.googleapis.com/drive/v3/files';
614 | url += '?' + this.enQuery(params);
615 | // console.log(params)
616 | let requestOption = await this.requestOption();
617 | let response = await fetch(url, requestOption);
618 | let res_obj = await response.json();
619 |
620 | return {
621 | nextPageToken: res_obj.nextPageToken || null,
622 | curPageIndex: page_index,
623 | data: res_obj
624 | };
625 | }
626 |
627 |
628 | /**
629 | * 一层一层的向上获取这个文件或文件夹的上级文件夹的 file 对象。注意:会很慢!!!
630 | * 最多向上寻找到当前 gd 对象的根目录 (root id)
631 | * 只考虑一条单独的向上链。
632 | * 【注意】如果此id代表的项目不在目标gd盘下,那么此函数会返回null
633 | *
634 | * @param child_id
635 | * @param contain_myself
636 | * @returns {Promise<[]>}
637 | */
638 | async findParentFilesRecursion(child_id, contain_myself = true) {
639 | const gd = this;
640 | const gd_root_id = gd.root.id;
641 | const user_drive_real_root_id = authConfig.user_drive_real_root_id;
642 | const is_user_drive = gd.root_type === CONSTS.gd_root_type.user_drive;
643 |
644 | // 自下向上查询的终点目标id
645 | const target_top_id = is_user_drive ? user_drive_real_root_id : gd_root_id;
646 | const fields = CONSTS.default_file_fields;
647 |
648 | // [{},{},...]
649 | const parent_files = [];
650 | let meet_top = false;
651 |
652 | async function addItsFirstParent(file_obj) {
653 | if (!file_obj) return;
654 | if (!file_obj.parents) return;
655 | if (file_obj.parents.length < 1) return;
656 |
657 | // ['','',...]
658 | let p_ids = file_obj.parents;
659 | if (p_ids && p_ids.length > 0) {
660 | // its first parent
661 | const first_p_id = p_ids[0];
662 | if (first_p_id === target_top_id) {
663 | meet_top = true;
664 | return;
665 | }
666 | const p_file_obj = await gd.findItemById(first_p_id);
667 | if (p_file_obj && p_file_obj.id) {
668 | parent_files.push(p_file_obj);
669 | await addItsFirstParent(p_file_obj);
670 | }
671 | }
672 | }
673 |
674 | const child_obj = await gd.findItemById(child_id);
675 | if (contain_myself) {
676 | parent_files.push(child_obj);
677 | }
678 | await addItsFirstParent(child_obj);
679 |
680 | return meet_top ? parent_files : null
681 | }
682 |
683 | /**
684 | * 获取相对于本盘根目录的path
685 | * @param child_id
686 | * @returns {Promise} 【注意】如果此id代表的项目不在目标gd盘下,那么此方法会返回空字符串""
687 | */
688 | async findPathById(child_id) {
689 | if (this.id_path_cache[child_id]) {
690 | return this.id_path_cache[child_id];
691 | }
692 |
693 | const p_files = await this.findParentFilesRecursion(child_id);
694 | if (!p_files || p_files.length < 1) return '';
695 |
696 | let cache = [];
697 | // 把查出来的每一级的path和id都缓存一下
698 | p_files.forEach((value, idx) => {
699 | const is_folder = idx === 0 ? (p_files[idx].mimeType === CONSTS.folder_mime_type) : true;
700 | let path = '/' + p_files.slice(idx).map(it => it.name).reverse().join('/');
701 | if (is_folder) path += '/';
702 | cache.push({id: p_files[idx].id, path: path})
703 | });
704 |
705 | cache.forEach((obj) => {
706 | this.id_path_cache[obj.id] = obj.path;
707 | this.paths[obj.path] = obj.id
708 | });
709 |
710 | /*const is_folder = p_files[0].mimeType === CONSTS.folder_mime_type;
711 | let path = '/' + p_files.map(it => it.name).reverse().join('/');
712 | if (is_folder) path += '/';*/
713 |
714 | return cache[0].path;
715 | }
716 |
717 |
718 | // 根据id获取file item
719 | async findItemById(id) {
720 | const is_user_drive = this.root_type === CONSTS.gd_root_type.user_drive;
721 | let url = `https://www.googleapis.com/drive/v3/files/${id}?fields=${CONSTS.default_file_fields}${is_user_drive ? '' : '&supportsAllDrives=true'}`;
722 | let requestOption = await this.requestOption();
723 | let res = await fetch(url, requestOption);
724 | return await res.json()
725 | }
726 |
727 | async findPathId(path) {
728 | let c_path = '/';
729 | let c_id = this.paths[c_path];
730 |
731 | let arr = path.trim('/').split('/');
732 | for (let name of arr) {
733 | c_path += name + '/';
734 |
735 | if (typeof this.paths[c_path] == 'undefined') {
736 | let id = await this._findDirId(c_id, name);
737 | this.paths[c_path] = id;
738 | }
739 |
740 | c_id = this.paths[c_path];
741 | if (c_id == undefined || c_id == null) {
742 | break;
743 | }
744 | }
745 | // console.log(this.paths);
746 | return this.paths[path];
747 | }
748 |
749 | async _findDirId(parent, name) {
750 | name = decodeURIComponent(name).replace(/\'/g, "\\'");
751 |
752 | // console.log("_findDirId", parent, name);
753 |
754 | if (parent == undefined) {
755 | return null;
756 | }
757 |
758 | let url = 'https://www.googleapis.com/drive/v3/files';
759 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true};
760 | params.q = `'${parent}' in parents and mimeType = 'application/vnd.google-apps.folder' and name = '${name}' and trashed = false`;
761 | params.fields = "nextPageToken, files(id, name, mimeType)";
762 | url += '?' + this.enQuery(params);
763 | let requestOption = await this.requestOption();
764 | let response = await fetch(url, requestOption);
765 | let obj = await response.json();
766 | if (obj.files[0] == undefined) {
767 | return null;
768 | }
769 | return obj.files[0].id;
770 | }
771 |
772 | async accessToken() {
773 | console.log("accessToken");
774 | if (this.authConfig.expires == undefined || this.authConfig.expires < Date.now()) {
775 | const obj = await this.fetchAccessToken();
776 | if (obj.access_token != undefined) {
777 | this.authConfig.accessToken = obj.access_token;
778 | this.authConfig.expires = Date.now() + 3500 * 1000;
779 | }
780 | }
781 | return this.authConfig.accessToken;
782 | }
783 |
784 | async fetchAccessToken() {
785 | console.log("fetchAccessToken");
786 | const url = "https://www.googleapis.com/oauth2/v4/token";
787 | const headers = {
788 | 'Content-Type': 'application/x-www-form-urlencoded'
789 | };
790 | const post_data = {
791 | 'client_id': this.authConfig.client_id,
792 | 'client_secret': this.authConfig.client_secret,
793 | 'refresh_token': this.authConfig.refresh_token,
794 | 'grant_type': 'refresh_token'
795 | }
796 |
797 | let requestOption = {
798 | 'method': 'POST',
799 | 'headers': headers,
800 | 'body': this.enQuery(post_data)
801 | };
802 |
803 | const response = await fetch(url, requestOption);
804 | return await response.json();
805 | }
806 |
807 | async fetch200(url, requestOption) {
808 | let response;
809 | for (let i = 0; i < 3; i++) {
810 | response = await fetch(url, requestOption);
811 | console.log(response.status);
812 | if (response.status != 403) {
813 | break;
814 | }
815 | await this.sleep(800 * (i + 1));
816 | }
817 | return response;
818 | }
819 |
820 | async requestOption(headers = {}, method = 'GET') {
821 | const accessToken = await this.accessToken();
822 | headers['authorization'] = 'Bearer ' + accessToken;
823 | return {'method': method, 'headers': headers};
824 | }
825 |
826 | enQuery(data) {
827 | const ret = [];
828 | for (let d in data) {
829 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
830 | }
831 | return ret.join('&');
832 | }
833 |
834 | sleep(ms) {
835 | return new Promise(function (resolve, reject) {
836 | let i = 0;
837 | setTimeout(function () {
838 | console.log('sleep' + ms);
839 | i++;
840 | if (i >= 2) reject(new Error('i>=2'));
841 | else resolve(i);
842 | }, ms);
843 | })
844 | }
845 | }
846 |
847 | String.prototype.trim = function (char) {
848 | if (char) {
849 | return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), '');
850 | }
851 | return this.replace(/^\s+|\s+$/g, '');
852 | };
853 |
--------------------------------------------------------------------------------
/themes/material/app.js:
--------------------------------------------------------------------------------
1 | // 在head 中 加载 必要静态
2 | document.write('');
3 | // markdown支持
4 | document.write('');
5 | document.write('');
6 | // add custome theme and darkmode
7 | if (UI.dark_mode) {
8 | document.write(``);
9 | }
10 |
11 | // 初始化页面,并载入必要资源
12 | function init() {
13 | document.siteName = $('title').html();
14 | $('body').addClass(`mdui-theme-primary-${UI.main_color} mdui-theme-accent-${UI.accent_color}`);
15 | var html = `
16 |
20 |
21 |
22 | `;
23 | $('body').html(html);
24 | }
25 |
26 | const Os = {
27 | isWindows: navigator.platform.toUpperCase().indexOf('WIN') > -1, // .includes
28 | isMac: navigator.platform.toUpperCase().indexOf('MAC') > -1,
29 | isMacLike: /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform),
30 | isIos: /(iPhone|iPod|iPad)/i.test(navigator.platform),
31 | isMobile: /Android|webOS|iPhone|iPad|iPod|iOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
32 | };
33 |
34 | function getDocumentHeight() {
35 | var D = document;
36 | return Math.max(
37 | D.body.scrollHeight, D.documentElement.scrollHeight,
38 | D.body.offsetHeight, D.documentElement.offsetHeight,
39 | D.body.clientHeight, D.documentElement.clientHeight
40 | );
41 | }
42 |
43 | function render(path) {
44 | if (path.indexOf("?") > 0) {
45 | path = path.substr(0, path.indexOf("?"));
46 | }
47 | title(path);
48 | nav(path);
49 | // .../0: 这种
50 | var reg = /\/\d+:$/g;
51 | if (window.MODEL.is_search_page) {
52 | // 用来存储一些滚动事件的状态
53 | window.scroll_status = {
54 | // 滚动事件是否已经绑定
55 | event_bound: false,
56 | // "滚动到底部,正在加载更多数据" 事件的锁
57 | loading_lock: false
58 | };
59 | render_search_result_list()
60 | } else if (path.match(reg) || path.substr(-1) == '/') {
61 | // 用来存储一些滚动事件的状态
62 | window.scroll_status = {
63 | // 滚动事件是否已经绑定
64 | event_bound: false,
65 | // "滚动到底部,正在加载更多数据" 事件的锁
66 | loading_lock: false
67 | };
68 | list(path);
69 | } else {
70 | file(path);
71 | }
72 | }
73 |
74 |
75 | // 渲染 title
76 | function title(path) {
77 | path = decodeURI(path);
78 | var cur = window.current_drive_order || 0;
79 | var drive_name = window.drive_names[cur];
80 | path = path.replace(`/${cur}:`, '');
81 | // $('title').html(document.siteName + ' - ' + path);
82 | var model = window.MODEL;
83 | if (model.is_search_page)
84 | $('title').html(`${document.siteName} - ${drive_name} - 搜索 ${model.q} 的结果`);
85 | else
86 | $('title').html(`${document.siteName} - ${drive_name} - ${path}`);
87 | }
88 |
89 | // 渲染导航栏
90 | function nav(path) {
91 | var model = window.MODEL;
92 | var html = "";
93 | var cur = window.current_drive_order || 0;
94 | html += `${document.siteName}`;
95 | var names = window.drive_names;
96 | /*html += ``;
97 | html += ``;*/
102 |
103 | // 修改为 select
104 | html += ``;
109 |
110 | if (!model.is_search_page) {
111 | var arr = path.trim('/').split('/');
112 | var p = '/';
113 | if (arr.length > 1) {
114 | arr.shift();
115 | for (i in arr) {
116 | var n = arr[i];
117 | n = decodeURI(n);
118 | p += n + '/';
119 | if (n == '') {
120 | break;
121 | }
122 | html += `chevron_right${n}`;
123 | }
124 | }
125 | }
126 | var search_text = model.is_search_page ? (model.q || '') : '';
127 | const isMobile = Os.isMobile;
128 | var search_bar = `
129 |
130 |
133 |
136 |
137 |
`;
138 |
139 | // 个人盘 或 团队盘
140 | if (model.root_type < 2) {
141 | // 显示搜索框
142 | html += search_bar;
143 | }
144 |
145 | $('#nav').html(html);
146 | mdui.mutation();
147 | mdui.updateTextFields();
148 | }
149 |
150 | /**
151 | * 发起列目录的 POST 请求
152 | * @param path Path
153 | * @param params Form params
154 | * @param resultCallback Success Result Callback
155 | * @param authErrorCallback Pass Error Callback
156 | */
157 | function requestListPath(path, params, resultCallback, authErrorCallback) {
158 | var p = {
159 | password: params['password'] || null,
160 | page_token: params['page_token'] || null,
161 | page_index: params['page_index'] || 0
162 | };
163 | $.post(path, p, function (data, status) {
164 | var res = jQuery.parseJSON(data);
165 | if (res && res.error && res.error.code == '401') {
166 | // 密码验证失败
167 | if (authErrorCallback) authErrorCallback(path)
168 | } else if (res && res.data) {
169 | if (resultCallback) resultCallback(res, path, p)
170 | }
171 | })
172 | }
173 |
174 | /**
175 | * 搜索 POST 请求
176 | * @param params Form params
177 | * @param resultCallback Success callback
178 | */
179 | function requestSearch(params, resultCallback) {
180 | var p = {
181 | q: params['q'] || null,
182 | page_token: params['page_token'] || null,
183 | page_index: params['page_index'] || 0
184 | };
185 | $.post(`/${window.current_drive_order}:search`, p, function (data, status) {
186 | var res = jQuery.parseJSON(data);
187 | if (res && res.data) {
188 | if (resultCallback) resultCallback(res, p)
189 | }
190 | })
191 | }
192 |
193 |
194 | // 渲染文件列表
195 | function list(path) {
196 | var content = `
197 |
198 |
199 |
200 |
201 | -
202 |
203 | 文件
204 | expand_more
205 |
206 |
207 | 修改时间
208 | expand_more
209 |
210 |
211 | 大小
212 | expand_more
213 |
214 |
215 |
216 |
217 |
222 |
223 | `;
224 | $('#content').html(content);
225 |
226 | var password = localStorage.getItem('password' + path);
227 | $('#list').html(``);
228 | $('#readme_md').hide().html('');
229 | $('#head_md').hide().html('');
230 |
231 | /**
232 | * 列目录请求成功返回数据后的回调
233 | * @param res 返回的结果(object)
234 | * @param path 请求的路径
235 | * @param prevReqParams 请求时所用的参数
236 | */
237 | function successResultCallback(res, path, prevReqParams) {
238 |
239 | // 把 nextPageToken 和 currentPageIndex 暂存在 list元素 中
240 | $('#list')
241 | .data('nextPageToken', res['nextPageToken'])
242 | .data('curPageIndex', res['curPageIndex']);
243 |
244 | // 移除 loading spinner
245 | $('#spinner').remove();
246 |
247 | if (res['nextPageToken'] === null) {
248 | // 如果是最后一页,取消绑定 scroll 事件,重置 scroll_status ,并 append 数据
249 | $(window).off('scroll');
250 | window.scroll_status.event_bound = false;
251 | window.scroll_status.loading_lock = false;
252 | append_files_to_list(path, res['data']['files']);
253 | } else {
254 | // 如果不是最后一页,append数据 ,并绑定 scroll 事件(如果还未绑定),更新 scroll_status
255 | append_files_to_list(path, res['data']['files']);
256 | if (window.scroll_status.event_bound !== true) {
257 | // 绑定事件,如果还未绑定
258 | $(window).on('scroll', function () {
259 | var scrollTop = $(this).scrollTop();
260 | var scrollHeight = getDocumentHeight();
261 | var windowHeight = $(this).height();
262 | // 滚到底部
263 | if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) {
264 | /*
265 | 滚到底部事件触发时,如果此时已经正在 loading 中,则忽略此次事件;
266 | 否则,去 loading,并占据 loading锁,表明 正在 loading 中
267 | */
268 | if (window.scroll_status.loading_lock === true) {
269 | return;
270 | }
271 | window.scroll_status.loading_lock = true;
272 |
273 | // 展示一个 loading spinner
274 | $(``)
275 | .insertBefore('#readme_md');
276 | mdui.updateSpinners();
277 | // mdui.mutation();
278 |
279 | let $list = $('#list');
280 | requestListPath(path, {
281 | password: prevReqParams['password'],
282 | page_token: $list.data('nextPageToken'),
283 | // 请求下一页
284 | page_index: $list.data('curPageIndex') + 1
285 | },
286 | successResultCallback,
287 | // 密码和之前相同。不会出现 authError
288 | null
289 | )
290 | }
291 | });
292 | window.scroll_status.event_bound = true
293 | }
294 | }
295 |
296 | // loading 成功,并成功渲染了新数据之后,释放 loading 锁,以便能继续处理 "滚动到底部" 事件
297 | if (window.scroll_status.loading_lock === true) {
298 | window.scroll_status.loading_lock = false
299 | }
300 | }
301 |
302 | // 开始从第1页请求数据
303 | requestListPath(path, {password: password},
304 | successResultCallback,
305 | function (path) {
306 | $('#spinner').remove();
307 | var pass = prompt("目录加密, 请输入密码", "");
308 | localStorage.setItem('password' + path, pass);
309 | if (pass != null && pass != "") {
310 | list(path);
311 | } else {
312 | history.go(-1);
313 | }
314 | });
315 | }
316 |
317 | /**
318 | * 把请求得来的新一页的数据追加到 list 中
319 | * @param path 路径
320 | * @param files 请求得来的结果
321 | */
322 | function append_files_to_list(path, files) {
323 | var $list = $('#list');
324 | // 是最后一页数据了吗?
325 | var is_lastpage_loaded = null === $list.data('nextPageToken');
326 | var is_firstpage = '0' == $list.data('curPageIndex');
327 |
328 | html = "";
329 | let targetFiles = [];
330 | for (i in files) {
331 | var item = files[i];
332 | var p = path + item.name + '/';
333 | if (item['size'] == undefined) {
334 | item['size'] = "";
335 | }
336 |
337 | item['modifiedTime'] = utc2beijing(item['modifiedTime']);
338 | item['size'] = formatFileSize(item['size']);
339 | if (item['mimeType'] == 'application/vnd.google-apps.folder') {
340 | html += `
341 |
342 | folder_open
343 | ${item.name}
344 |
345 | ${item['modifiedTime']}
346 | ${item['size']}
347 |
348 | `;
349 | } else {
350 | var p = path + item.name;
351 | const filepath = path + item.name;
352 | var c = "file";
353 | // 当加载完最后一页后,才显示 README ,否则会影响滚动事件
354 | if (is_lastpage_loaded && item.name == "README.md") {
355 | get_file(p, item, function (data) {
356 | markdown("#readme_md", data);
357 | });
358 | }
359 | if (item.name == "HEAD.md") {
360 | get_file(p, item, function (data) {
361 | markdown("#head_md", data);
362 | });
363 | }
364 | var ext = p.split('.').pop().toLowerCase();
365 | if ("|html|php|css|go|java|js|json|txt|sh|md|mp4|webm|avi|bmp|jpg|jpeg|png|gif|m4a|mp3|flac|wav|ogg|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|pdf|".indexOf(`|${ext}|`) >= 0) {
366 | targetFiles.push(filepath);
367 | p += "?a=view";
368 | c += " view";
369 | }
370 | html += `
371 |
372 | insert_drive_file
373 | ${item.name}
374 |
375 | ${item['modifiedTime']}
376 | ${item['size']}
377 |
378 | `;
379 | }
380 | }
381 |
382 | /*let targetObj = {};
383 | targetFiles.forEach((myFilepath, myIndex) => {
384 | if (!targetObj[myFilepath]) {
385 | targetObj[myFilepath] = {
386 | filepath: myFilepath,
387 | prev: myIndex === 0 ? null : targetFiles[myIndex - 1],
388 | next: myIndex === targetFiles.length - 1 ? null : targetFiles[myIndex + 1],
389 | }
390 | }
391 | })
392 | // console.log(targetObj)
393 | if (Object.keys(targetObj).length) {
394 | localStorage.setItem(path, JSON.stringify(targetObj));
395 | // console.log(path)
396 | }*/
397 |
398 | if (targetFiles.length > 0) {
399 | let old = localStorage.getItem(path);
400 | let new_children = targetFiles;
401 | // 第1页重设;否则追加
402 | if (!is_firstpage && old) {
403 | let old_children;
404 | try {
405 | old_children = JSON.parse(old);
406 | if (!Array.isArray(old_children)) {
407 | old_children = []
408 | }
409 | } catch (e) {
410 | old_children = [];
411 | }
412 | new_children = old_children.concat(targetFiles)
413 | }
414 |
415 | localStorage.setItem(path, JSON.stringify(new_children))
416 | }
417 |
418 | // 是第1页时,去除横向loading条
419 | $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html);
420 | // 是最后一页时,统计并显示出总项目数
421 | if (is_lastpage_loaded) {
422 | $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length);
423 | }
424 | }
425 |
426 | /**
427 | * 渲染搜索结果列表。有大量重复代码,但是里面有不一样的逻辑,暂时先这样分开弄吧
428 | */
429 | function render_search_result_list() {
430 | var content = `
431 |
432 |
433 |
434 |
435 | -
436 |
437 | 文件
438 | expand_more
439 |
440 |
441 | 修改时间
442 | expand_more
443 |
444 |
445 | 大小
446 | expand_more
447 |
448 |
449 |
450 |
451 |
456 |
457 | `;
458 | $('#content').html(content);
459 |
460 | $('#list').html(``);
461 | $('#readme_md').hide().html('');
462 | $('#head_md').hide().html('');
463 |
464 | /**
465 | * 搜索请求成功返回数据后的回调
466 | * @param res 返回的结果(object)
467 | * @param path 请求的路径
468 | * @param prevReqParams 请求时所用的参数
469 | */
470 | function searchSuccessCallback(res, prevReqParams) {
471 |
472 | // 把 nextPageToken 和 currentPageIndex 暂存在 list元素 中
473 | $('#list')
474 | .data('nextPageToken', res['nextPageToken'])
475 | .data('curPageIndex', res['curPageIndex']);
476 |
477 | // 移除 loading spinner
478 | $('#spinner').remove();
479 |
480 | if (res['nextPageToken'] === null) {
481 | // 如果是最后一页,取消绑定 scroll 事件,重置 scroll_status ,并 append 数据
482 | $(window).off('scroll');
483 | window.scroll_status.event_bound = false;
484 | window.scroll_status.loading_lock = false;
485 | append_search_result_to_list(res['data']['files']);
486 | } else {
487 | // 如果不是最后一页,append数据 ,并绑定 scroll 事件(如果还未绑定),更新 scroll_status
488 | append_search_result_to_list(res['data']['files']);
489 | if (window.scroll_status.event_bound !== true) {
490 | // 绑定事件,如果还未绑定
491 | $(window).on('scroll', function () {
492 | var scrollTop = $(this).scrollTop();
493 | var scrollHeight = getDocumentHeight();
494 | var windowHeight = $(this).height();
495 | // 滚到底部
496 | if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) {
497 | /*
498 | 滚到底部事件触发时,如果此时已经正在 loading 中,则忽略此次事件;
499 | 否则,去 loading,并占据 loading锁,表明 正在 loading 中
500 | */
501 | if (window.scroll_status.loading_lock === true) {
502 | return;
503 | }
504 | window.scroll_status.loading_lock = true;
505 |
506 | // 展示一个 loading spinner
507 | $(``)
508 | .insertBefore('#readme_md');
509 | mdui.updateSpinners();
510 | // mdui.mutation();
511 |
512 | let $list = $('#list');
513 | requestSearch({
514 | q: window.MODEL.q,
515 | page_token: $list.data('nextPageToken'),
516 | // 请求下一页
517 | page_index: $list.data('curPageIndex') + 1
518 | },
519 | searchSuccessCallback
520 | )
521 | }
522 | });
523 | window.scroll_status.event_bound = true
524 | }
525 | }
526 |
527 | // loading 成功,并成功渲染了新数据之后,释放 loading 锁,以便能继续处理 "滚动到底部" 事件
528 | if (window.scroll_status.loading_lock === true) {
529 | window.scroll_status.loading_lock = false
530 | }
531 | }
532 |
533 | // 开始从第1页请求数据
534 | requestSearch({q: window.MODEL.q}, searchSuccessCallback);
535 | }
536 |
537 | /**
538 | * 追加新一页的搜索结果
539 | * @param files
540 | */
541 | function append_search_result_to_list(files) {
542 | var $list = $('#list');
543 | // 是最后一页数据了吗?
544 | var is_lastpage_loaded = null === $list.data('nextPageToken');
545 | // var is_firstpage = '0' == $list.data('curPageIndex');
546 |
547 | html = "";
548 |
549 | for (i in files) {
550 | var item = files[i];
551 | if (item['size'] == undefined) {
552 | item['size'] = "";
553 | }
554 |
555 | item['modifiedTime'] = utc2beijing(item['modifiedTime']);
556 | item['size'] = formatFileSize(item['size']);
557 | if (item['mimeType'] == 'application/vnd.google-apps.folder') {
558 | html += `
559 |
560 | folder_open
561 | ${item.name}
562 |
563 | ${item['modifiedTime']}
564 | ${item['size']}
565 |
566 | `;
567 | } else {
568 | var c = "file";
569 | var ext = item.name.split('.').pop().toLowerCase();
570 | if ("|html|php|css|go|java|js|json|txt|sh|md|mp4|webm|avi|bmp|jpg|jpeg|png|gif|m4a|mp3|flac|wav|ogg|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|".indexOf(`|${ext}|`) >= 0) {
571 | c += " view";
572 | }
573 | html += `
574 |
575 | insert_drive_file
576 | ${item.name}
577 |
578 | ${item['modifiedTime']}
579 | ${item['size']}
580 |
581 | `;
582 | }
583 | }
584 |
585 | // 是第1页时,去除横向loading条
586 | $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html);
587 | // 是最后一页时,统计并显示出总项目数
588 | if (is_lastpage_loaded) {
589 | $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length);
590 | }
591 | }
592 |
593 | /**
594 | * 搜索结果项目点击事件
595 | * @param a_ele 点击的元素
596 | */
597 | function onSearchResultItemClick(a_ele) {
598 | var me = $(a_ele);
599 | var can_preview = me.hasClass('view');
600 | var cur = window.current_drive_order;
601 | var dialog = mdui.dialog({
602 | title: '',
603 | content: '正在获取目标路径...
',
604 | // content: '',
605 | history: false,
606 | modal: true,
607 | closeOnEsc: true
608 | });
609 | mdui.updateSpinners();
610 |
611 | // 请求获取路径
612 | $.post(`/${cur}:id2path`, {id: a_ele.id}, function (data) {
613 | if (data) {
614 | dialog.close();
615 | var href = `/${cur}:${data}${can_preview ? '?a=view' : ''}`;
616 | dialog = mdui.dialog({
617 | title: '目标路径',
618 | content: `${data}`,
619 | history: false,
620 | modal: true,
621 | closeOnEsc: true,
622 | buttons: [
623 | {
624 | text: '打开', onClick: function () {
625 | window.location.href = href
626 | }
627 | }, {
628 | text: '新标签中打开', onClick: function () {
629 | window.open(href)
630 | }
631 | }
632 | , {text: '取消'}
633 | ]
634 | });
635 | return;
636 | }
637 | dialog.close();
638 | dialog = mdui.dialog({
639 | title: '获取目标路径失败',
640 | content: 'o(╯□╰)o 可能是因为该盘中并不存在此项!也可能因为没有把【与我共享】的文件添加到个人云端硬盘中!',
641 | history: false,
642 | modal: true,
643 | closeOnEsc: true,
644 | buttons: [
645 | {text: 'WTF ???'}
646 | ]
647 | });
648 | })
649 | }
650 |
651 | function get_file(path, file, callback) {
652 | var key = "file_path_" + path + file['modifiedTime'];
653 | var data = localStorage.getItem(key);
654 | if (data != undefined) {
655 | return callback(data);
656 | } else {
657 | $.get(path, function (d) {
658 | localStorage.setItem(key, d);
659 | callback(d);
660 | });
661 | }
662 | }
663 |
664 |
665 | // 文件展示 ?a=view
666 | function file(path) {
667 | var name = path.split('/').pop();
668 | var ext = name.split('.').pop().toLowerCase().replace(`?a=view`, "").toLowerCase();
669 | if ("|html|php|css|go|java|js|json|txt|sh|md|".indexOf(`|${ext}|`) >= 0) {
670 | return file_code(path);
671 | }
672 |
673 | if ("|mp4|webm|avi|".indexOf(`|${ext}|`) >= 0) {
674 | return file_video(path);
675 | }
676 |
677 | if ("|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|".indexOf(`|${ext}|`) >= 0) {
678 | return file_video(path);
679 | }
680 |
681 | if ("|mp3|flac|wav|ogg|m4a|".indexOf(`|${ext}|`) >= 0) {
682 | return file_audio(path);
683 | }
684 |
685 | if ("|bmp|jpg|jpeg|png|gif|".indexOf(`|${ext}|`) >= 0) {
686 | return file_image(path);
687 | }
688 |
689 | if ('pdf' === ext) return file_pdf(path);
690 | }
691 |
692 | // 文件展示 |html|php|css|go|java|js|json|txt|sh|md|
693 | function file_code(path) {
694 | var type = {
695 | "html": "html",
696 | "php": "php",
697 | "css": "css",
698 | "go": "golang",
699 | "java": "java",
700 | "js": "javascript",
701 | "json": "json",
702 | "txt": "Text",
703 | "sh": "sh",
704 | "md": "Markdown",
705 | };
706 | var name = path.split('/').pop();
707 | var ext = name.split('.').pop().toLowerCase();
708 | var href = window.location.origin + path;
709 | var content = `
710 |
713 |
714 |
715 |
716 |
717 | file_download
718 |
719 |
720 |
721 | `;
722 | $('#content').html(content);
723 |
724 | $.get(path, function (data) {
725 | $('#editor').html($('').text(data).html());
726 | var code_type = "Text";
727 | if (type[ext] != undefined) {
728 | code_type = type[ext];
729 | }
730 | var editor = ace.edit("editor");
731 | editor.setTheme("ace/theme/ambiance");
732 | editor.setFontSize(18);
733 | editor.session.setMode("ace/mode/" + code_type);
734 |
735 | //Autocompletion
736 | editor.setOptions({
737 | enableBasicAutocompletion: true,
738 | enableSnippets: true,
739 | enableLiveAutocompletion: true,
740 | maxLines: Infinity
741 | });
742 | });
743 | }
744 |
745 | function copyToClipboard(str) {
746 | const $temp = $("");
747 | $("body").append($temp);
748 | $temp.val(str).select();
749 | document.execCommand("copy");
750 | $temp.remove();
751 | }
752 |
753 | // 文件展示 视频 |mp4|webm|avi|
754 | function file_video(path) {
755 | const url = window.location.origin + path;
756 | let player_items = [
757 | {
758 | text: 'MXPlayer(Free)',
759 | href: `intent:${url}#Intent;package=com.mxtech.videoplayer.ad;S.title=${path};end`,
760 | },
761 | {
762 | text: 'MXPlayer(Pro)',
763 | href: `intent:${url}#Intent;package=com.mxtech.videoplayer.pro;S.title=${path};end`,
764 | },
765 | {
766 | text: 'nPlayer',
767 | href: `nplayer-${url}`,
768 | },
769 | {
770 | text: 'VLC',
771 | href: `vlc://${url}`,
772 | },
773 | {
774 | text: 'PotPlayer',
775 | href: `potplayer://${url}`
776 | }
777 | ]
778 | .map(it => ``)
779 | .join('');
780 | player_items += `
781 | `;
782 | const playBtn = `
783 |
786 | `;
787 |
788 | const content = `
789 |
790 |
791 |
794 |
${playBtn}
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 | file_download
806 | `;
807 | $('#content').html(content);
808 | $('#copy-link').on('click', () => {
809 | copyToClipboard(url);
810 | mdui.snackbar('已复制到剪切板!');
811 | });
812 | }
813 |
814 | // 文件展示 音频 |mp3|flac|m4a|wav|ogg|
815 | function file_audio(path) {
816 | var url = window.location.origin + path;
817 | var content = `
818 |
819 |
820 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 | file_download
835 | `;
836 | $('#content').html(content);
837 | }
838 |
839 | // 文件展示 pdf pdf
840 | function file_pdf(path) {
841 | const url = window.location.origin + path;
842 | const inline_url = `${url}?inline=true`
843 | const file_name = decodeURI(path.slice(path.lastIndexOf('/') + 1, path.length))
844 | var content = `
845 |
846 | file_download
847 | `;
848 | $('#content').removeClass('mdui-container').addClass('mdui-container-fluid').css({padding: 0}).html(content);
849 | }
850 |
851 | // 图片展示
852 | function file_image(path) {
853 | var url = window.location.origin + path;
854 | // console.log(window.location.pathname)
855 | const currentPathname = window.location.pathname
856 | const lastIndex = currentPathname.lastIndexOf('/');
857 | const fatherPathname = currentPathname.slice(0, lastIndex + 1);
858 | // console.log(fatherPathname)
859 | let target_children = localStorage.getItem(fatherPathname);
860 | // console.log(`fatherPathname: ${fatherPathname}`);
861 | // console.log(target_children)
862 | let targetText = '';
863 | if (target_children) {
864 | try {
865 | target_children = JSON.parse(target_children);
866 | if (!Array.isArray(target_children)) {
867 | target_children = []
868 | }
869 | } catch (e) {
870 | console.error(e);
871 | target_children = [];
872 | }
873 | if (target_children.length > 0 && target_children.includes(path)) {
874 | let len = target_children.length;
875 | let cur = target_children.indexOf(path);
876 | // console.log(`len = ${len}`)
877 | // console.log(`cur = ${cur}`)
878 | let prev_child = (cur - 1 > -1) ? target_children[cur - 1] : null;
879 | let next_child = (cur + 1 < len) ? target_children[cur + 1] : null;
880 | targetText = `
881 |
882 |
883 |
884 | ${prev_child ? `` : ``}
885 |
886 |
887 | ${next_child ? `` : ``}
888 |
889 |
890 |
891 | `;
892 | }
893 | //
894 | // ${targetObj[path].prev ? `Prev` : `Prev`}
895 | // ${targetObj[path].next ? `Next` : `Prev`}
896 | //
897 | }
898 | var content = `
899 |
900 |
901 |
902 | ${targetText}
903 |

904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 | file_download
921 | `;
922 | //my code
923 | $('#content').html(content);
924 | $('#leftBtn, #rightBtn').click((e) => {
925 | let target = $(e.target);
926 | if (['I', 'SPAN'].includes(e.target.nodeName)) {
927 | target = $(e.target).parent();
928 | }
929 | const filepath = target.attr('data-filepath');
930 | const direction = target.attr('data-direction');
931 | //console.log(`${direction}翻页 ${filepath}`);
932 | file(filepath)
933 | });
934 | }
935 |
936 |
937 | //时间转换
938 | function utc2beijing(utc_datetime) {
939 | // 转为正常的时间格式 年-月-日 时:分:秒
940 | var T_pos = utc_datetime.indexOf('T');
941 | var Z_pos = utc_datetime.indexOf('Z');
942 | var year_month_day = utc_datetime.substr(0, T_pos);
943 | var hour_minute_second = utc_datetime.substr(T_pos + 1, Z_pos - T_pos - 1);
944 | var new_datetime = year_month_day + " " + hour_minute_second; // 2017-03-31 08:02:06
945 |
946 | // 处理成为时间戳
947 | timestamp = new Date(Date.parse(new_datetime));
948 | timestamp = timestamp.getTime();
949 | timestamp = timestamp / 1000;
950 |
951 | // 增加8个小时,北京时间比utc时间多八个时区
952 | var unixtimestamp = timestamp + 8 * 60 * 60;
953 |
954 | // 时间戳转为时间
955 | var unixtimestamp = new Date(unixtimestamp * 1000);
956 | var year = 1900 + unixtimestamp.getYear();
957 | var month = "0" + (unixtimestamp.getMonth() + 1);
958 | var date = "0" + unixtimestamp.getDate();
959 | var hour = "0" + unixtimestamp.getHours();
960 | var minute = "0" + unixtimestamp.getMinutes();
961 | var second = "0" + unixtimestamp.getSeconds();
962 | return year + "-" + month.substring(month.length - 2, month.length) + "-" + date.substring(date.length - 2, date.length)
963 | + " " + hour.substring(hour.length - 2, hour.length) + ":"
964 | + minute.substring(minute.length - 2, minute.length) + ":"
965 | + second.substring(second.length - 2, second.length);
966 | }
967 |
968 | // bytes自适应转换到KB,MB,GB
969 | function formatFileSize(bytes) {
970 | if (bytes >= 1000000000) {
971 | bytes = (bytes / 1000000000).toFixed(2) + ' GB';
972 | } else if (bytes >= 1000000) {
973 | bytes = (bytes / 1000000).toFixed(2) + ' MB';
974 | } else if (bytes >= 1000) {
975 | bytes = (bytes / 1000).toFixed(2) + ' KB';
976 | } else if (bytes > 1) {
977 | bytes = bytes + ' bytes';
978 | } else if (bytes == 1) {
979 | bytes = bytes + ' byte';
980 | } else {
981 | bytes = '';
982 | }
983 | return bytes;
984 | }
985 |
986 | String.prototype.trim = function (char) {
987 | if (char) {
988 | return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), '');
989 | }
990 | return this.replace(/^\s+|\s+$/g, '');
991 | };
992 |
993 |
994 | // README.md HEAD.md 支持
995 | function markdown(el, data) {
996 | if (window.md == undefined) {
997 | //$.getScript('https://cdn.jsdelivr.net/npm/markdown-it@10.0.0/dist/markdown-it.min.js',function(){
998 | window.md = window.markdownit();
999 | markdown(el, data);
1000 | //});
1001 | } else {
1002 | var html = md.render(data);
1003 | $(el).show().html(html);
1004 | }
1005 | }
1006 |
1007 | // 监听回退事件
1008 | window.onpopstate = function () {
1009 | var path = window.location.pathname;
1010 | render(path);
1011 | }
1012 |
1013 |
1014 | $(function () {
1015 | init();
1016 | var path = window.location.pathname;
1017 | /*$("body").on("click", '.folder', function () {
1018 | var url = $(this).attr('href');
1019 | history.pushState(null, null, url);
1020 | render(url);
1021 | return false;
1022 | });
1023 |
1024 | $("body").on("click", '.view', function () {
1025 | var url = $(this).attr('href');
1026 | history.pushState(null, null, url);
1027 | render(url);
1028 | return false;
1029 | });*/
1030 |
1031 | render(path);
1032 | });
1033 |
--------------------------------------------------------------------------------