// https://github.com/donwa/goindex/blob/master/使用及免责协议.md
var authConfig = {
"siteName": "GoIndex", // 网站名称
"root_pass": "index", // 根目录密码,优先于.password
"version" : "1.0.6", // 程序版本
"theme" : "material", // material classic
"client_id": "202264815644.apps.googleusercontent.com",
"client_secret": "X4Z3ca8xfWDb1Voo-F9a7ZxJ",
"refresh_token": "", // 授权 token
"root": "root" // 根目录ID
};
var gd;
var html = `
${authConfig.siteName}
`;
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
/**
* Fetch and log a request
* @param {Request} request
*/
async function handleRequest(request) {
if(gd == undefined){
gd = new googleDrive(authConfig);
}
if(request.method == 'POST'){
return apiRequest(request);
}
let url = new URL(request.url);
let path = url.pathname;
let action = url.searchParams.get('a');
if(path.substr(-1) == '/'){
try {
await gd.list(path);
} catch (e) {
return new Response("", { status: 404 }); // if path: /notexist/
}
return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
} else if(action != null){
if (await gd.file(path) == undefined){
return new Response(html404, { status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
}
return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
} else {
if (path.split('/').pop().toLowerCase() == ".password") {
return new Response("", { status: 404 });
}
try {
await gd.file(path);
} catch (e) {
return new Response("", { status: 404 }); // if path: /notexist/notexist
}
let file = await gd.file(path);
if (file == undefined){
return new Response("", { status: 404 }); // if path: /exist/notexist
}
let range = request.headers.get('Range');
return gd.down(file.id, range);
}
}
async function apiRequest(request) {
let url = new URL(request.url);
let path = url.pathname;
let option = {status:200,headers:{'Access-Control-Allow-Origin':'*'}}
if(path.substr(-1) == '/'){
// check password
let password = await gd.password(path);
console.log("dir password", password);
if(password != undefined && password != null && password != ""){
try{
var obj = await request.json();
}catch(e){
var obj = {};
}
console.log(password,obj);
if(password.replace("\n", "") != obj.password){
let html = `{"error": {"code": 401,"message": "password error."}}`;
return new Response(html,option);
}
}
let list = await gd.list(path);
return new Response(JSON.stringify(list),option);
}else{
let file = await gd.file(path);
let range = request.headers.get('Range');
return new Response(JSON.stringify(file));
}
}
class googleDrive {
constructor(authConfig) {
this.authConfig = authConfig;
this.paths = [];
this.files = [];
this.passwords = [];
this.paths["/"] = authConfig.root;
if(authConfig.root_pass != ""){
this.passwords["/"] = authConfig.root_pass;
}
this.accessToken();
}
async down(id, range=''){
let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`;
let requestOption = await this.requestOption();
requestOption.headers['Range'] = range;
return await fetch(url, requestOption);
}
async file(path){
if(typeof this.files[path] == 'undefined'){
this.files[path] = await this._file(path);
}
return this.files[path] ;
}
async _file(path){
let arr = path.split('/');
let name = arr.pop();
name = decodeURIComponent(name).replace(/\'/g, "\\'");
let dir = arr.join('/')+'/';
console.log(name, dir);
let parent = await this.findPathId(dir);
console.log(parent);
let url = 'https://www.googleapis.com/drive/v3/files';
let params = {'includeItemsFromAllDrives':true,'supportsAllDrives':true};
params.q = `'${parent}' in parents and name = '${name}' andtrashed = false`;
params.fields = "files(id, name, mimeType, size ,createdTime, modifiedTime, iconLink, thumbnailLink, shortcutDetails)";
url += '?'+this.enQuery(params);
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
let obj = await response.json();
if (obj.files && obj.files[0] && obj.files[0].mimeType == 'application/vnd.google-apps.shortcut'){
obj.files[0].id = obj.files[0].shortcutDetails.targetId;
obj.files[0].mimeType = obj.files[0].shortcutDetails.targetMimeType;
}
console.log(obj);
return obj.files[0];
}
// 通过reqeust cache 来缓存
async list(path){
if (gd.cache == undefined) {
gd.cache = {};
}
if (gd.cache[path]) {
return gd.cache[path];
}
let id = await this.findPathId(path);
var obj = await this._ls(id);
if (obj.files && obj.files.length > 1000) {
gd.cache[path] = obj;
}
return obj
}
async password(path){
if(this.passwords[path] !== undefined){
return this.passwords[path];
}
console.log("load",path,".password",this.passwords[path]);
let file = await gd.file(path+'.password');
if(file == undefined){
this.passwords[path] = null;
}else{
let url = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`;
let requestOption = await this.requestOption();
let response = await this.fetch200(url, requestOption);
this.passwords[path] = await response.text();
}
return this.passwords[path];
}
async _ls(parent){
console.log("_ls",parent);
if(parent==undefined){
return null;
}
const files = [];
let pageToken;
let obj;
let params = {'includeItemsFromAllDrives':true,'supportsAllDrives':true};
params.q = `'${parent}' in parents and trashed = false AND name !='.password'`;
params.orderBy= 'folder,name,modifiedTime desc';
params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime, shortcutDetails)";
params.pageSize = 1000;
do {
if (pageToken) {
params.pageToken = pageToken;
}
let url = 'https://www.googleapis.com/drive/v3/files';
url += '?'+this.enQuery(params);
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
obj = await response.json();
obj.files.forEach(file => {
if (file && file.mimeType == 'application/vnd.google-apps.shortcut') {
file.id = file.shortcutDetails.targetId;
file.mimeType = file.shortcutDetails.targetMimeType;
}
});
files.push(...obj.files);
pageToken = obj.nextPageToken;
} while (pageToken);
obj.files = files;
return obj;
}
async findPathId(path){
let c_path = '/';
let c_id = this.paths[c_path];
let arr = path.trim('/').split('/');
for(let name of arr){
c_path += name+'/';
if(typeof this.paths[c_path] == 'undefined'){
let id = await this._findDirId(c_id, name);
this.paths[c_path] = id;
}
c_id = this.paths[c_path];
if(c_id == undefined || c_id == null){
break;
}
}
console.log(this.paths);
return this.paths[path];
}
async _findDirId(parent, name){
name = decodeURIComponent(name).replace(/\'/g, "\\'");
console.log("_findDirId",parent,name);
if(parent==undefined){
return null;
}
let url = 'https://www.googleapis.com/drive/v3/files';
let params = {'includeItemsFromAllDrives':true,'supportsAllDrives':true};
params.q = `'${parent}' in parents and (mimeType = 'application/vnd.google-apps.folder' or mimeType = 'application/vnd.google-apps.shortcut') and name = '${name}' and trashed = false`;
params.fields = "nextPageToken, files(id, name, mimeType, shortcutDetails)";
url += '?'+this.enQuery(params);
let requestOption = await this.requestOption();
let response = await fetch(url, requestOption);
let obj = await response.json();
if(obj.files[0] == undefined){
return null;
}
if (obj.files[0].mimeType == 'application/vnd.google-apps.shortcut' && obj.files[0].shortcutDetails.targetMimeType == 'application/vnd.google-apps.folder') {
obj.files[0].id = obj.files[0].shortcutDetails.targetId;
} else if (obj.files[0].mimeType == 'application/vnd.google-apps.shortcut' && obj.files[0].shortcutDetails.targetMimeType != 'application/vnd.google-apps.folder'){
return null;
}
return obj.files[0].id;
}
async accessToken(){
console.log("accessToken");
if(this.authConfig.expires == undefined ||this.authConfig.expires< Date.now()){
const obj = await this.fetchAccessToken();
if(obj.access_token != undefined){
this.authConfig.accessToken = obj.access_token;
this.authConfig.expires = Date.now()+3500*1000;
}
}
return this.authConfig.accessToken;
}
async fetchAccessToken() {
console.log("fetchAccessToken");
const url = "https://www.googleapis.com/oauth2/v4/token";
const headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
const post_data = {
'client_id': this.authConfig.client_id,
'client_secret': this.authConfig.client_secret,
'refresh_token': this.authConfig.refresh_token,
'grant_type': 'refresh_token'
}
let requestOption = {
'method': 'POST',
'headers': headers,
'body': this.enQuery(post_data)
};
const response = await fetch(url, requestOption);
return await response.json();
}
async fetch200(url, requestOption) {
let response;
for (let i = 0; i < 3; i++) {
response = await fetch(url, requestOption);
console.log(response.status);
if (response.status != 403) {
break;
}
await this.sleep(800 * (i + 1));
}
return response;
}
async requestOption(headers={},method='GET'){
const accessToken = await this.accessToken();
headers['authorization'] = 'Bearer '+ accessToken;
return {'method': method, 'headers':headers};
}
enQuery(data) {
const ret = [];
for (let d in data) {
ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
}
return ret.join('&');
}
sleep(ms) {
return new Promise(function (resolve, reject) {
let i = 0;
setTimeout(function () {
console.log('sleep' + ms);
i++;
if (i >= 2) reject(new Error('i>=2'));
else resolve(i);
}, ms);
})
}
}
String.prototype.trim = function (char) {
if (char) {
return this.replace(new RegExp('^\\'+char+'+|\\'+char+'+$', 'g'), '');
}
return this.replace(/^\s+|\s+$/g, '');
};