diff --git a/Search/index.js b/Search/index.js new file mode 100644 index 0000000..bbb324d --- /dev/null +++ b/Search/index.js @@ -0,0 +1,797 @@ +var authConfig = { + "siteName": "G-Index", // WebSite Name + "version": "1.0", // VersionControl, do not modify manually + // Only material! + "theme": "material", // material classic + //add themes color, darkmode + "main_color": "", // red | pink | purple | deep-purple | indigo | blue | light-blue | cyan | teal | green | light-green | lime yellow | amber orange | deep-orange | brown | greyblue-grey + "accent_color": "", // red | pink | purple | deep-purple | indigo | blue | light-blue | cyan | teal | green | light-green | lime | yellow | amber | orange | deep-orange + "dark_theme": "", // True for dark theme + // client_id & client_secret + "client_id": "", + "client_secret": "", + "refresh_token": "", // Refresh token + + /** + * Set up multiple Drives to display; add multiples by format + * id can be team disk id, subfolder id, or "root" (representing the root directory of individual disk) + * name - The displayed name + * pass is the corresponding password, which can be set separately, or an empty string if no password is required + * [Note] For the disk whose id is set to the subfolder id, the search function will not be supported (it does not affect other disks) + */ + "roots": [ + { + id: "root", + name: "Sample1", + pass: "" + }, + { + id: "drive_id", + name: "Sample2", + pass: "index" + }, + { + id: "folder_id", + name: "Sample3", + pass: "index2" + } + ], + /** + * The number displayed on each page of the file list page. [Recommended setting value is between 100 and 1000]; +    * If the setting is greater than 1000, it will cause an error when requesting drive api; +    * If the set value is too small, the incremental loading (page loading) of the scroll bar of the file list page will be invalid +    * Another effect of this value is that if the number of files in the directory is greater than this setting (that is, multiple pages need to be displayed), the results of the first listing directory will be cached. + */ + "files_list_page_size": 500, + /** + * The number displayed on each page of the search results page. [Recommended setting value is between 50 and 1000]; +    * If the setting is greater than 1000, it will cause an error when requesting drive api; +    * If the set value is too small, it will cause the incremental loading (page loading) of the scroll bar of the search results page to fail; +    * The size of this value affects the response speed of the search operation + */ + "search_result_list_page_size": 50, + // Confirm that cors can be opened + "enable_cors_file_down": false + // user_drive_real_root_id +}; + + +/** + * global functions + */ +const FUNCS = { + /** + * Transform into relatively safe search keywords for Google search morphology + */ + formatSearchKeyword: function (keyword) { + let nothing = ""; + let space = " "; + if (!keyword) return nothing; + return keyword.replace(/(!=)|['"=<>/\\:]/g, nothing) + .replace(/[,,|(){}]/g, space) + .trim() + } + +}; + +/** + * global consts + * @type {{folder_mime_type: string, default_file_fields: string, gd_root_type: {share_drive: number, user_drive: number, sub_folder: number}}} + */ +const CONSTS = new (class { + default_file_fields = 'parents,id,name,mimeType,modifiedTime,createdTime,fileExtension,size'; + gd_root_type = { + user_drive: 0, + share_drive: 1, + sub_folder: 2 + }; + folder_mime_type = 'application/vnd.google-apps.folder'; +})(); + + +// gd instances +var gds = []; + +function html(current_drive_order = 0, model = {}) { + return ` + + + + + + ${authConfig.siteName} + + + + + + + + +`; +}; + +addEventListener('fetch', event => { + event.respondWith(handleRequest(event.request)); +}); + +/** + * Fetch and log a request + * @param {Request} request + */ +async function handleRequest(request) { + if (gds.length === 0) { + for (let i = 0; i < authConfig.roots.length; i++) { + const gd = new googleDrive(authConfig, i); + await gd.init(); + gds.push(gd) + } + // This operation is parallel to improve efficiency + let tasks = []; + gds.forEach(gd => { + tasks.push(gd.initRootType()); + }); + for (let task of tasks) { + await task; + } + } + + // Extract drive order from path + // and get the corresponding gd instance according to the drive order + let gd; + let url = new URL(request.url); + let path = url.pathname; + + /** + * Redirect to start page + * @returns {Response} + */ + function redirectToIndexPage() { + return new Response('', {status: 301, headers: {'Location': `${url.origin}/0:/`}}); + } + + if (path == '/') return redirectToIndexPage(); + if (path.toLowerCase() == '/favicon.ico') { + // You can find one later favicon + return new Response('', {status: 404}) + } + + // Special command format + const command_reg = /^\/(?\d+):(?[a-zA-Z0-9]+)$/g; + const match = command_reg.exec(path); + if (match) { + const num = match.groups.num; + const order = Number(num); + if (order >= 0 && order < gds.length) { + gd = gds[order]; + } else { + return redirectToIndexPage() + } + const command = match.groups.command; + // Search + if (command === 'search') { + if (request.method === 'POST') { + // search results + return handleSearch(request, gd); + } else { + const params = url.searchParams; + // Search Page + return new Response(html(gd.order, { + q: params.get("q") || '', + is_search_page: true, + root_type: gd.root_type + }), + { + status: 200, + headers: {'Content-Type': 'text/html; charset=utf-8'} + }); + } + } else if (command === 'id2path' && request.method === 'POST') { + return handleId2Path(request, gd) + } + } + + // Desired path format + const common_reg = /^\/\d+:\/.*$/g; + try { + if (!path.match(common_reg)) { + return redirectToIndexPage(); + } + let split = path.split("/"); + let order = Number(split[1].slice(0, -1)); + if (order >= 0 && order < gds.length) { + gd = gds[order]; + } else { + return redirectToIndexPage() + } + } catch (e) { + return redirectToIndexPage() + } + + path = path.replace(gd.url_path_prefix, '') || '/'; + if (request.method == 'POST') { + return apiRequest(request, gd); + } + + let action = url.searchParams.get('a'); + + if (path.substr(-1) == '/' || action != null) { + return new Response(html(gd.order, {root_type: gd.root_type}), { + status: 200, + headers: {'Content-Type': 'text/html; charset=utf-8'} + }); + } else { + if (path.split('/').pop().toLowerCase() == ".password") { + return new Response("", {status: 404}); + } + let file = await gd.file(path); + let range = request.headers.get('Range'); + const inline_down = 'true' === url.searchParams.get('inline'); + return gd.down(file.id, range, inline_down); + } +} + + +async function apiRequest(request, gd) { + let url = new URL(request.url); + let path = url.pathname; + path = path.replace(gd.url_path_prefix, '') || '/'; + + let option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}} + + if (path.substr(-1) == '/') { + let deferred_pass = gd.password(path); + let form = await request.formData(); + // This can increase the speed of the first listing. The disadvantage is that if the password verification fails, the overhead of listing directories will still be incurred + let deferred_list_result = gd.list(path, form.get('page_token'), Number(form.get('page_index'))); + + // check password + let password = await deferred_pass; + // console.log("dir password", password); + if (password != undefined && password != null && password != "") { + if (password.replace("\n", "") != form.get('password')) { + let html = `{"error": {"code": 401,"message": "password error."}}`; + return new Response(html, option); + } + } + + let list_result = await deferred_list_result; + return new Response(JSON.stringify(list_result), option); + } else { + let file = await gd.file(path); + let range = request.headers.get('Range'); + return new Response(JSON.stringify(file)); + } +} + +// Deal With search +async function handleSearch(request, gd) { + const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}; + let form = await request.formData(); + let search_result = await + gd.search(form.get('q') || '', form.get('page_token'), Number(form.get('page_index'))); + return new Response(JSON.stringify(search_result), option); +} + +/** + * Deal With id2path + * @param request Id parameter required + * @param gd + * @returns {Promise} [Note] If the item represented by the id received from the front desk is not under the target gd disk, then the response will be returned to the front desk with an empty string "" + */ +async function handleId2Path(request, gd) { + const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}; + let form = await request.formData(); + let path = await gd.findPathById(form.get('id')); + return new Response(path || '', option); +} + +class googleDrive { + constructor(authConfig, order) { + // Each disk corresponds to an order, corresponding to a gd instance + this.order = order; + this.root = authConfig.roots[order]; + this.url_path_prefix = `/${order}:`; + this.authConfig = authConfig; + // TODO: The invalid refresh strategy of these caches can be formulated later + // path id + this.paths = []; + // path file + this.files = []; + // path pass + this.passwords = []; + // id <-> path + this.id_path_cache = {}; + this.id_path_cache[this.root['id']] = '/'; + this.paths["/"] = this.root['id']; + if (this.root['pass'] != "") { + this.passwords['/'] = this.root['pass']; + } + // this.init(); + } + + /** + * Initial authorization; then obtain user_drive_real_root_id + * @returns {Promise} + */ + async init() { + await this.accessToken(); + /*await (async () => { + // Get only 1 time + if (authConfig.user_drive_real_root_id) return; + const root_obj = await (gds[0] || this).findItemById('root'); + if (root_obj && root_obj.id) { + authConfig.user_drive_real_root_id = root_obj.id + } + })();*/ + // Wait for user_drive_real_root_id and only get it once + if (authConfig.user_drive_real_root_id) return; + const root_obj = await (gds[0] || this).findItemById('root'); + if (root_obj && root_obj.id) { + authConfig.user_drive_real_root_id = root_obj.id + } + } + + /** + * Get the root directory type, set to root_type + * @returns {Promise} + */ + async initRootType() { + const root_id = this.root['id']; + const types = CONSTS.gd_root_type; + if (root_id === 'root' || root_id === authConfig.user_drive_real_root_id) { + this.root_type = types.user_drive; + } else { + const obj = await this.getShareDriveObjById(root_id); + this.root_type = obj ? types.share_drive : types.sub_folder; + } + } + + async down(id, range = '', inline = false) { + let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`; + let requestOption = await this.requestOption(); + requestOption.headers['Range'] = range; + let res = await fetch(url, requestOption); + const {headers} = res = new Response(res.body, res) + this.authConfig.enable_cors_file_down && headers.append('Access-Control-Allow-Origin', '*'); + inline === true && headers.set('Content-Disposition', 'inline'); + return res; + } + + 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}' and trashed = false`; + params.fields = "files(id, name, mimeType, size ,createdTime, modifiedTime, iconLink, thumbnailLink)"; + url += '?' + this.enQuery(params); + let requestOption = await this.requestOption(); + let response = await fetch(url, requestOption); + let obj = await response.json(); + // console.log(obj); + return obj.files[0]; + } + + // Cache through reqeust cache + async list(path, page_token = null, page_index = 0) { + if (this.path_children_cache == undefined) { + // { :[ {nextPageToken:'',data:{}}, {nextPageToken:'',data:{}} ...], ...} + this.path_children_cache = {}; + } + + if (this.path_children_cache[path] + && this.path_children_cache[path][page_index] + && this.path_children_cache[path][page_index].data + ) { + let child_obj = this.path_children_cache[path][page_index]; + return { + nextPageToken: child_obj.nextPageToken || null, + curPageIndex: page_index, + data: child_obj.data + }; + } + + let id = await this.findPathId(path); + let result = await this._ls(id, page_token, page_index); + let data = result.data; + // Cache multiple pages + if (result.nextPageToken && data.files) { + if (!Array.isArray(this.path_children_cache[path])) { + this.path_children_cache[path] = [] + } + this.path_children_cache[path][Number(result.curPageIndex)] = { + nextPageToken: result.nextPageToken, + data: data + }; + } + + return result + } + + + async _ls(parent, page_token = null, page_index = 0) { + // console.log("_ls", parent); + + if (parent == undefined) { + return null; + } + 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)"; + params.pageSize = this.authConfig.files_list_page_size; + + if (page_token) { + params.pageToken = page_token; + } + 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(); + + return { + nextPageToken: obj.nextPageToken || null, + curPageIndex: page_index, + data: obj + }; + + /*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(); + files.push(...obj.files); + pageToken = obj.nextPageToken; + } while (pageToken);*/ + + } + + async password(path) { + if (this.passwords[path] !== undefined) { + return this.passwords[path]; + } + + // console.log("load", path, ".password", this.passwords[path]); + + let file = await this.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]; + } + + + /** + * Get share drive information by id + * @param any_id + * @returns {Promise} Any abnormal situation returns null + */ + async getShareDriveObjById(any_id) { + if (!any_id) return null; + if ('string' !== typeof any_id) return null; + + let url = `https://www.googleapis.com/drive/v3/drives/${any_id}`; + let requestOption = await this.requestOption(); + let res = await fetch(url, requestOption); + let obj = await res.json(); + if (obj && obj.id) return obj; + + return null + } + + + /** + * search for + * @returns {Promise<{data: null, nextPageToken: null, curPageIndex: number}>} + */ + async search(origin_keyword, page_token = null, page_index = 0) { + const types = CONSTS.gd_root_type; + const is_user_drive = this.root_type === types.user_drive; + const is_share_drive = this.root_type === types.share_drive; + + const empty_result = { + nextPageToken: null, + curPageIndex: page_index, + data: null + }; + + if (!is_user_drive && !is_share_drive) { + return empty_result; + } + let keyword = FUNCS.formatSearchKeyword(origin_keyword); + if (!keyword) { + // The keyword is empty, return + return empty_result; + } + let words = keyword.split(/\s+/); + let name_search_str = `name contains '${words.join("' AND name contains '")}'`; + + // corpora is a personal drive for user and a team drive for drive. With driveId + let params = {}; + if (is_user_drive) { + params.corpora = 'user' + } + if (is_share_drive) { + params.corpora = 'drive'; + params.driveId = this.root.id; + // This parameter will only be effective until June 1, 2020. Afterwards shared drive items will be included in the results. + params.includeItemsFromAllDrives = true; + params.supportsAllDrives = true; + } + if (page_token) { + params.pageToken = page_token; + } + params.q = `trashed = false AND name !='.password' AND (${name_search_str})`; + params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime)"; + params.pageSize = this.authConfig.search_result_list_page_size; + // params.orderBy = 'folder,name,modifiedTime desc'; + + let url = 'https://www.googleapis.com/drive/v3/files'; + url += '?' + this.enQuery(params); + // console.log(params) + let requestOption = await this.requestOption(); + let response = await fetch(url, requestOption); + let res_obj = await response.json(); + + return { + nextPageToken: res_obj.nextPageToken || null, + curPageIndex: page_index, + data: res_obj + }; + } + + + /** + * Get the file object of the superior folder of this file or folder layer by layer. Note: It will be very slow! ! ! + * Up to find the root directory (root id) of the current gd object + * Only consider a single upward chain. + * [Note] If the item represented by this id is not under the target gd disk, then this function will return null + * + * @param child_id + * @param contain_myself + * @returns {Promise<[]>} + */ + async findParentFilesRecursion(child_id, contain_myself = true) { + const gd = this; + const gd_root_id = gd.root.id; + const user_drive_real_root_id = authConfig.user_drive_real_root_id; + const is_user_drive = gd.root_type === CONSTS.gd_root_type.user_drive; + + // End point query id from bottom to top + const target_top_id = is_user_drive ? user_drive_real_root_id : gd_root_id; + const fields = CONSTS.default_file_fields; + + // [{},{},...] + const parent_files = []; + let meet_top = false; + + async function addItsFirstParent(file_obj) { + if (!file_obj) return; + if (!file_obj.parents) return; + if (file_obj.parents.length < 1) return; + + // ['','',...] + let p_ids = file_obj.parents; + if (p_ids && p_ids.length > 0) { + // its first parent + const first_p_id = p_ids[0]; + if (first_p_id === target_top_id) { + meet_top = true; + return; + } + const p_file_obj = await gd.findItemById(first_p_id); + if (p_file_obj && p_file_obj.id) { + parent_files.push(p_file_obj); + await addItsFirstParent(p_file_obj); + } + } + } + + const child_obj = await gd.findItemById(child_id); + if (contain_myself) { + parent_files.push(child_obj); + } + await addItsFirstParent(child_obj); + + return meet_top ? parent_files : null + } + + /** + * Get the path relative to the root directory of this disk + * @param child_id + * @returns {Promise} [Note] If the item represented by this id is not in the target gd disk, then this method will return an empty string "" + */ + async findPathById(child_id) { + if (this.id_path_cache[child_id]) { + return this.id_path_cache[child_id]; + } + + const p_files = await this.findParentFilesRecursion(child_id); + if (!p_files || p_files.length < 1) return ''; + + let cache = []; + // Cache the path and id of each level found + p_files.forEach((value, idx) => { + const is_folder = idx === 0 ? (p_files[idx].mimeType === CONSTS.folder_mime_type) : true; + let path = '/' + p_files.slice(idx).map(it => it.name).reverse().join('/'); + if (is_folder) path += '/'; + cache.push({id: p_files[idx].id, path: path}) + }); + + cache.forEach((obj) => { + this.id_path_cache[obj.id] = obj.path; + this.paths[obj.path] = obj.id + }); + + /*const is_folder = p_files[0].mimeType === CONSTS.folder_mime_type; + let path = '/' + p_files.map(it => it.name).reverse().join('/'); + if (is_folder) path += '/';*/ + + return cache[0].path; + } + + + // Get file item based on id + async findItemById(id) { + const is_user_drive = this.root_type === CONSTS.gd_root_type.user_drive; + let url = `https://www.googleapis.com/drive/v3/files/${id}?fields=${CONSTS.default_file_fields}${is_user_drive ? '' : '&supportsAllDrives=true'}`; + let requestOption = await this.requestOption(); + let res = await fetch(url, requestOption); + return await res.json() + } + + 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' and name = '${name}' and trashed = false`; + params.fields = "nextPageToken, files(id, name, mimeType)"; + 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; + } + 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, ''); +}; diff --git a/Search/themes/material/app.js b/Search/themes/material/app.js new file mode 100644 index 0000000..9c17e42 --- /dev/null +++ b/Search/themes/material/app.js @@ -0,0 +1,1034 @@ +// Load the necessary static in the head +document.write(''); +// markdown Standby +document.write(''); +document.write(''); + +if(dark){document.write('');} + +// Initialize the page and load the necessary resources +function init(){ + document.siteName = $('title').html(); + $('body').addClass("mdui-theme-primary-"+main_color+" mdui-theme-accent-"+accent_color); + var html = ""; + html += ` +
` + if(dark){ + html += ` + `; + }else{ + html += ` + `; + } +html += ` +
+
+
`; + $('body').html(html); +} + +const Os = { + isWindows: navigator.platform.toUpperCase().indexOf('WIN') > -1, // .includes + isMac: navigator.platform.toUpperCase().indexOf('MAC') > -1, + isMacLike: /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform), + isIos: /(iPhone|iPod|iPad)/i.test(navigator.platform), + isMobile: /Android|webOS|iPhone|iPad|iPod|iOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) +}; + +function getDocumentHeight() { + var D = document; + return Math.max( + D.body.scrollHeight, D.documentElement.scrollHeight, + D.body.offsetHeight, D.documentElement.offsetHeight, + D.body.clientHeight, D.documentElement.clientHeight + ); +} + +function render(path) { + if (path.indexOf("?") > 0) { + path = path.substr(0, path.indexOf("?")); + } + title(path); + nav(path); + // .../0: This + var reg = /\/\d+:$/g; + if (window.MODEL.is_search_page) { + // Used to store the state of some scroll events + window.scroll_status = { + // Whether the scroll event is already bound + event_bound: false, + // "Scroll to the bottom, loading more data" event lock + loading_lock: false + }; + render_search_result_list() + } else if (path.match(reg) || path.substr(-1) == '/') { + // Used to store the state of some scroll events + window.scroll_status = { + // Whether the scroll event is already bound + event_bound: false, + // "Scroll to the bottom, loading more data" event lock + loading_lock: false + }; + list(path); + } else { + file(path); + } +} + + +// Rendering title +function title(path) { + path = decodeURI(path); + var cur = window.current_drive_order || 0; + var drive_name = window.drive_names[cur]; + path = path.replace(`/${cur}:`, ''); + // $('title').html(document.siteName + ' - ' + path); + var model = window.MODEL; + if (model.is_search_page) + $('title').html(`${document.siteName} - ${drive_name} - 搜索 ${model.q} 的结果`); + else + $('title').html(`${document.siteName} - ${drive_name} - ${path}`); +} + +// Render the navigation bar +function nav(path) { + var model = window.MODEL; + var html = ""; + var cur = window.current_drive_order || 0; + html += `${document.siteName}`; + var names = window.drive_names; + /*html += ``; + html += `
    `; + names.forEach((name, idx) => { + html += `
  • ${name}
  • `; + }); + html += `
`;*/ + + // change into select + html += ``; + + if (!model.is_search_page) { + var arr = path.trim('/').split('/'); + var p = '/'; + if (arr.length > 1) { + arr.shift(); + for (i in arr) { + var n = arr[i]; + n = decodeURI(n); + p += n + '/'; + if (n == '') { + break; + } + html += `chevron_right${n}`; + } + } + } + var search_text = model.is_search_page ? (model.q || '') : ''; + const isMobile = Os.isMobile; + var search_bar = `
+ `; + + // Personal or team + if (model.root_type < 2) { + // Show search box + html += search_bar; + } + + $('#nav').html(html); + mdui.mutation(); + mdui.updateTextFields(); +} + +/** + * Initiate POST request for listing + * @param path Path + * @param params Form params + * @param resultCallback Success Result Callback + * @param authErrorCallback Pass Error Callback + */ +function requestListPath(path, params, resultCallback, authErrorCallback) { + var p = { + password: params['password'] || null, + page_token: params['page_token'] || null, + page_index: params['page_index'] || 0 + }; + $.post(path, p, function (data, status) { + var res = jQuery.parseJSON(data); + if (res && res.error && res.error.code == '401') { + // Password verification failed + if (authErrorCallback) authErrorCallback(path) + } else if (res && res.data) { + if (resultCallback) resultCallback(res, path, p) + } + }) +} + +/** + * Search POST request + * @param params Form params + * @param resultCallback Success callback + */ +function requestSearch(params, resultCallback) { + var p = { + q: params['q'] || null, + page_token: params['page_token'] || null, + page_index: params['page_index'] || 0 + }; + $.post(`/${window.current_drive_order}:search`, p, function (data, status) { + var res = jQuery.parseJSON(data); + if (res && res.data) { + if (resultCallback) resultCallback(res, p) + } + }) +} + + +// Render file list +function list(path) { + var content = ` + +
+
    +
  • +
    + File + expand_more +
    +
    + Time + expand_more +
    +
    + Size + expand_more +
    +
  • +
+
+
+
    +
+
Total Item
+
+ + `; + $('#content').html(content); + + var password = localStorage.getItem('password' + path); + $('#list').html(`
`); + $('#readme_md').hide().html(''); + $('#head_md').hide().html(''); + + /** + * Callback after successful data return from column directory request + * @param res Returned result (object) + * @param path Requested path + * @param prevReqParams Parameters used in the request + */ + function successResultCallback(res, path, prevReqParams) { + + // Temporarily store nextPageToken and currentPageIndex in the list element + $('#list') + .data('nextPageToken', res['nextPageToken']) + .data('curPageIndex', res['curPageIndex']); + + // Remove loading spinner + $('#spinner').remove(); + + if (res['nextPageToken'] === null) { + // If it is the last page, unbind the scroll event, reset scroll_status, and append data + $(window).off('scroll'); + window.scroll_status.event_bound = false; + window.scroll_status.loading_lock = false; + append_files_to_list(path, res['data']['files']); + } else { + // If it is not the last page, append data and bind the scroll event (if not already bound), update scroll_status + append_files_to_list(path, res['data']['files']); + if (window.scroll_status.event_bound !== true) { + // Bind event, if not yet bound + $(window).on('scroll', function () { + var scrollTop = $(this).scrollTop(); + var scrollHeight = getDocumentHeight(); + var windowHeight = $(this).height(); + // Roll to the bottom + if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) { + /* + When the event of scrolling to the bottom is triggered, if it is already loading at this time, the event is ignored; + Otherwise, go to loading and occupy the loading lock, indicating that loading is in progress + */ + if (window.scroll_status.loading_lock === true) { + return; + } + window.scroll_status.loading_lock = true; + + // Show one loading spinner + $(`
`) + .insertBefore('#readme_md'); + mdui.updateSpinners(); + // mdui.mutation(); + + let $list = $('#list'); + requestListPath(path, { + password: prevReqParams['password'], + page_token: $list.data('nextPageToken'), + // Request next page + page_index: $list.data('curPageIndex') + 1 + }, + successResultCallback, + // The password is the same as before. Will not appear authError + null + ) + } + }); + window.scroll_status.event_bound = true + } + } + + // After loading successfully and rendering new data successfully, release the loading lock so that you can continue to process the "scroll to bottom" event + if (window.scroll_status.loading_lock === true) { + window.scroll_status.loading_lock = false + } + } + + // Start requesting data from page 1 + requestListPath(path, {password: password}, + successResultCallback, + function (path) { + $('#spinner').remove(); + var pass = prompt("Access Denied, please enter the password", ""); + localStorage.setItem('password' + path, pass); + if (pass != null && pass != "") { + list(path); + } else { + history.go(-1); + } + }); +} + +/** + * Append the data of the new page requested to the list + * @param path path + * @param files Requested results + */ +function append_files_to_list(path, files) { + var $list = $('#list'); + // Is it the last page of data? + var is_lastpage_loaded = null === $list.data('nextPageToken'); + var is_firstpage = '0' == $list.data('curPageIndex'); + + html = ""; + let targetFiles = []; + for (i in files) { + var item = files[i]; + var p = path + item.name + '/'; + if (item['size'] == undefined) { + item['size'] = ""; + } + + item['modifiedTime'] = utc2beijing(item['modifiedTime']); + item['size'] = formatFileSize(item['size']); + if (item['mimeType'] == 'application/vnd.google-apps.folder') { + html += `
  • +
    + folder_open + ${item.name} +
    +
    ${item['modifiedTime']}
    +
    ${item['size']}
    +
    +
  • `; + } else { + var p = path + item.name; + const filepath = path + item.name; + var c = "file"; + // README is displayed after the last page is loaded, otherwise it will affect the scroll event + if (is_lastpage_loaded && item.name == "README.md") { + get_file(p, item, function (data) { + markdown("#readme_md", data); + }); + } + if (item.name == "HEAD.md") { + get_file(p, item, function (data) { + markdown("#head_md", data); + }); + } + var ext = p.split('.').pop().toLowerCase(); + 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) { + targetFiles.push(filepath); + p += "?a=view"; + c += " view"; + } + html += `
  • +
    + insert_drive_file + ${item.name} +
    +
    ${item['modifiedTime']}
    +
    ${item['size']}
    +
    +
  • `; + } + } + + /*let targetObj = {}; + targetFiles.forEach((myFilepath, myIndex) => { + if (!targetObj[myFilepath]) { + targetObj[myFilepath] = { + filepath: myFilepath, + prev: myIndex === 0 ? null : targetFiles[myIndex - 1], + next: myIndex === targetFiles.length - 1 ? null : targetFiles[myIndex + 1], + } + } + }) + // console.log(targetObj) + if (Object.keys(targetObj).length) { + localStorage.setItem(path, JSON.stringify(targetObj)); + // console.log(path) + }*/ + + if (targetFiles.length > 0) { + let old = localStorage.getItem(path); + let new_children = targetFiles; + // Reset on page 1; otherwise append + if (!is_firstpage && old) { + let old_children; + try { + old_children = JSON.parse(old); + if (!Array.isArray(old_children)) { + old_children = [] + } + } catch (e) { + old_children = []; + } + new_children = old_children.concat(targetFiles) + } + + localStorage.setItem(path, JSON.stringify(new_children)) + } + + // When it is page 1, remove the horizontal loading bar + $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html); + // When it is the last page, count and display the total number of items + if (is_lastpage_loaded) { + $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length); + } +} + +/** + * Render the search result list. There is a lot of repetitive code, but there are different logics in it. + */ +function render_search_result_list() { + var content = ` + +
    +
      +
    • +
      + File + expand_more +
      +
      + Time + expand_more +
      +
      + Size + expand_more +
      +
    • +
    +
    +
    +
      +
    +
    Total Item
    +
    + + `; + $('#content').html(content); + + $('#list').html(`
    `); + $('#readme_md').hide().html(''); + $('#head_md').hide().html(''); + + /** + * The callback after the search request successfully returns data + * @param res Results returned(object) + * @param path Requested path + * @param prevReqParams Parameters used in the request + */ + function searchSuccessCallback(res, prevReqParams) { + + // Temporarily store nextPageToken and currentPageIndex in the list element + $('#list') + .data('nextPageToken', res['nextPageToken']) + .data('curPageIndex', res['curPageIndex']); + + // Removeloading spinner + $('#spinner').remove(); + + if (res['nextPageToken'] === null) { + // If it is the last page, unbind the scroll event, reset scroll_status, and append data + $(window).off('scroll'); + window.scroll_status.event_bound = false; + window.scroll_status.loading_lock = false; + append_search_result_to_list(res['data']['files']); + } else { + // If it is not the last page, append data and bind the scroll event (if not already bound), update scroll_status + append_search_result_to_list(res['data']['files']); + if (window.scroll_status.event_bound !== true) { + // Bind event, if not yet bound + $(window).on('scroll', function () { + var scrollTop = $(this).scrollTop(); + var scrollHeight = getDocumentHeight(); + var windowHeight = $(this).height(); + // Roll to the bottom + if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) { + /* + When the event of scrolling to the bottom is triggered, if it is already loading at this time, the event is ignored; + Otherwise, go to loading and occupy the loading lock, indicating that loading is in progress + */ + if (window.scroll_status.loading_lock === true) { + return; + } + window.scroll_status.loading_lock = true; + + // Show one loading spinner + $(`
    `) + .insertBefore('#readme_md'); + mdui.updateSpinners(); + // mdui.mutation(); + + let $list = $('#list'); + requestSearch({ + q: window.MODEL.q, + page_token: $list.data('nextPageToken'), + // Request next page + page_index: $list.data('curPageIndex') + 1 + }, + searchSuccessCallback + ) + } + }); + window.scroll_status.event_bound = true + } + } + + // After loading successfully and rendering new data successfully, release the loading lock so that you can continue to process the "scroll to bottom" event + if (window.scroll_status.loading_lock === true) { + window.scroll_status.loading_lock = false + } + } + + // Start requesting data from page 1 + requestSearch({q: window.MODEL.q}, searchSuccessCallback); +} + +/** + * Append a new page of search results + * @param files + */ +function append_search_result_to_list(files) { + var $list = $('#list'); + // Is it the last page of data? + var is_lastpage_loaded = null === $list.data('nextPageToken'); + // var is_firstpage = '0' == $list.data('curPageIndex'); + + html = ""; + + for (i in files) { + var item = files[i]; + if (item['size'] == undefined) { + item['size'] = ""; + } + + item['modifiedTime'] = utc2beijing(item['modifiedTime']); + item['size'] = formatFileSize(item['size']); + if (item['mimeType'] == 'application/vnd.google-apps.folder') { + html += `
  • +
    + folder_open + ${item.name} +
    +
    ${item['modifiedTime']}
    +
    ${item['size']}
    +
    +
  • `; + } else { + var c = "file"; + var ext = item.name.split('.').pop().toLowerCase(); + 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) { + c += " view"; + } + html += `
  • +
    + insert_drive_file + ${item.name} +
    +
    ${item['modifiedTime']}
    +
    ${item['size']}
    +
    +
  • `; + } + } + + // When it is page 1, remove the horizontal loading bar + $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html); + // When it is the last page, count and display the total number of items + if (is_lastpage_loaded) { + $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length); + } +} + +/** + * Search result item click event + * @param a_ele Clicked element + */ +function onSearchResultItemClick(a_ele) { + var me = $(a_ele); + var can_preview = me.hasClass('view'); + var cur = window.current_drive_order; + var dialog = mdui.dialog({ + title: '', + content: '
    Getting target path...
    ', + // content: '
    ', + history: false, + modal: true, + closeOnEsc: true + }); + mdui.updateSpinners(); + + // Request to get the path + $.post(`/${cur}:id2path`, {id: a_ele.id}, function (data) { + if (data) { + dialog.close(); + var href = `/${cur}:${data}${can_preview ? '?a=view' : ''}`; + dialog = mdui.dialog({ + title: 'Target path', + content: `${data}`, + history: false, + modal: true, + closeOnEsc: true, + buttons: [ + { + text: 'Open', onClick: function () { + window.location.href = href + } + }, { + text: 'Open in new tab', onClick: function () { + window.open(href) + } + } + , {text: 'Cancel'} + ] + }); + return; + } + dialog.close(); + dialog = mdui.dialog({ + title: 'Failed to get the target path', + content: 'o(╯□╰)o It may be because this item does not exist in the disk! It may also be because the file [Shared with me] has not been added to Personal Drive!', + history: false, + modal: true, + closeOnEsc: true, + buttons: [ + {text: 'WTF ???'} + ] + }); + }) +} + +function get_file(path, file, callback) { + var key = "file_path_" + path + file['modifiedTime']; + var data = localStorage.getItem(key); + if (data != undefined) { + return callback(data); + } else { + $.get(path, function (d) { + localStorage.setItem(key, d); + callback(d); + }); + } +} + + +// File display? A = view +function file(path) { + var name = path.split('/').pop(); + var ext = name.split('.').pop().toLowerCase().replace(`?a=view`, "").toLowerCase(); + if ("|html|php|css|go|java|js|json|txt|sh|md|".indexOf(`|${ext}|`) >= 0) { + return file_code(path); + } + + if ("|mp4|webm|avi|".indexOf(`|${ext}|`) >= 0) { + return file_video(path); + } + + if ("|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|".indexOf(`|${ext}|`) >= 0) { + return file_video(path); + } + + if ("|mp3|flac|wav|ogg|m4a|".indexOf(`|${ext}|`) >= 0) { + return file_audio(path); + } + + if ("|bmp|jpg|jpeg|png|gif|".indexOf(`|${ext}|`) >= 0) { + return file_image(path); + } + + if ('pdf' === ext) return file_pdf(path); +} + +// Document display |html|php|css|go|java|js|json|txt|sh|md| +function file_code(path) { + var type = { + "html": "html", + "php": "php", + "css": "css", + "go": "golang", + "java": "java", + "js": "javascript", + "json": "json", + "txt": "Text", + "sh": "sh", + "md": "Markdown", + }; + var name = path.split('/').pop(); + var ext = name.split('.').pop().toLowerCase(); + var href = window.location.origin + path; + var content = ` +
    +
    
    +
    +
    + + +
    +file_download + + + `; + $('#content').html(content); + + $.get(path, function (data) { + $('#editor').html($('
    ').text(data).html()); + var code_type = "Text"; + if (type[ext] != undefined) { + code_type = type[ext]; + } + var editor = ace.edit("editor"); + editor.setTheme("ace/theme/ambiance"); + editor.setFontSize(18); + editor.session.setMode("ace/mode/" + code_type); + + //Autocompletion + editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + enableLiveAutocompletion: true, + maxLines: Infinity + }); + }); +} +function copyToClipboard(str) { + const $temp = $(""); + $("body").append($temp); + $temp.val(str).select(); + document.execCommand("copy"); + $temp.remove(); +} +// Document display video |mp4|webm|avi| +function file_video(path) { + const url = window.location.origin + path; + let player_items = [ + { + text: 'MXPlayer(Free)', + href: `intent:${url}#Intent;package=com.mxtech.videoplayer.ad;S.title=${path};end`, + }, + { + text: 'MXPlayer(Pro)', + href: `intent:${url}#Intent;package=com.mxtech.videoplayer.pro;S.title=${path};end`, + }, + { + text: 'nPlayer', + href: `nplayer-${url}`, + }, + { + text: 'VLC', + href: `vlc://${url}`, + }, + { + text: 'PotPlayer', + href: `potplayer://${url}` + } + ] + .map(it => `
  • ${it.text}
  • `) + .join(''); + player_items += `
  • +
  • Copy Link
  • `; + const playBtn = ` + + +
      ${player_items}
    `; + + const content = ` + +
    +
    + +
    ${playBtn} + +
    + + +
    +
    + + +
    +
    +file_download + `; + $('#content').html(content); + $('#copy-link').on('click', () => { + copyToClipboard(url); + mdui.snackbar('Copied To Clipboard!'); + }); +} + +// File display Audio |mp3|flac|m4a|wav|ogg| +function file_audio(path) { + var url = window.location.origin + path; + var content = ` +
    +
    + +
    + +
    + + +
    +
    + + +
    +
    +file_download + `; + $('#content').html(content); +} + +// Document display pdf pdf +function file_pdf(path) { + const url = window.location.origin + path; + const inline_url = `${url}?inline=true` + const file_name = decodeURI(path.slice(path.lastIndexOf('/') + 1, path.length)) + var content = ` + + file_download + `; + $('#content').removeClass('mdui-container').addClass('mdui-container-fluid').css({padding: 0}).html(content); +} + +// picture display +function file_image(path) { + var url = window.location.origin + path; + // console.log(window.location.pathname) + const currentPathname = window.location.pathname + const lastIndex = currentPathname.lastIndexOf('/'); + const fatherPathname = currentPathname.slice(0, lastIndex + 1); + // console.log(fatherPathname) + let target_children = localStorage.getItem(fatherPathname); + // console.log(`fatherPathname: ${fatherPathname}`); + // console.log(target_children) + let targetText = ''; + if (target_children) { + try { + target_children = JSON.parse(target_children); + if (!Array.isArray(target_children)) { + target_children = [] + } + } catch (e) { + console.error(e); + target_children = []; + } + if (target_children.length > 0 && target_children.includes(path)) { + let len = target_children.length; + let cur = target_children.indexOf(path); + // console.log(`len = ${len}`) + // console.log(`cur = ${cur}`) + let prev_child = (cur - 1 > -1) ? target_children[cur - 1] : null; + let next_child = (cur + 1 < len) ? target_children[cur + 1] : null; + targetText = ` +
    +
    +
    + ${prev_child ? `` : ``} +
    +
    + ${next_child ? `` : ``} +
    +
    +
    + `; + } + //
    + // ${targetObj[path].prev ? `Prev` : `Prev`} + // ${targetObj[path].next ? `Next` : `Prev`} + //
    + } + var content = ` +
    +
    +
    + ${targetText} + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +file_download + `; + //my code + $('#content').html(content); + $('#leftBtn, #rightBtn').click((e) => { + let target = $(e.target); + if (['I', 'SPAN'].includes(e.target.nodeName)) { + target = $(e.target).parent(); + } + const filepath = target.attr('data-filepath'); + const direction = target.attr('data-direction'); + //console.log (`$ {direction} turn page $ {filepath}`); + file(filepath) + }); +} + + +//Time conversion +function utc2beijing(utc_datetime) { + // Convert to normal time format year-month-day hour: minute: second + var T_pos = utc_datetime.indexOf('T'); + var Z_pos = utc_datetime.indexOf('Z'); + var year_month_day = utc_datetime.substr(0, T_pos); + var hour_minute_second = utc_datetime.substr(T_pos + 1, Z_pos - T_pos - 1); + var new_datetime = year_month_day + " " + hour_minute_second; // 2017-03-31 08:02:06 + + // Processing becomes timestamp + timestamp = new Date(Date.parse(new_datetime)); + timestamp = timestamp.getTime(); + timestamp = timestamp / 1000; + + // 8 hours more, Beijing time is eight more time zones than UTC time + var unixtimestamp = timestamp + 8 * 60 * 60; + + // Timestamp to time + var unixtimestamp = new Date(unixtimestamp * 1000); + var year = 1900 + unixtimestamp.getYear(); + var month = "0" + (unixtimestamp.getMonth() + 1); + var date = "0" + unixtimestamp.getDate(); + var hour = "0" + unixtimestamp.getHours(); + var minute = "0" + unixtimestamp.getMinutes(); + var second = "0" + unixtimestamp.getSeconds(); + return year + "-" + month.substring(month.length - 2, month.length) + "-" + date.substring(date.length - 2, date.length) + + " " + hour.substring(hour.length - 2, hour.length) + ":" + + minute.substring(minute.length - 2, minute.length) + ":" + + second.substring(second.length - 2, second.length); +} + +// bytes Adaptive conversion to KB, MB, GB +function formatFileSize(bytes) { + if (bytes >= 1000000000) { + bytes = (bytes / 1000000000).toFixed(2) + ' GB'; + } else if (bytes >= 1000000) { + bytes = (bytes / 1000000).toFixed(2) + ' MB'; + } else if (bytes >= 1000) { + bytes = (bytes / 1000).toFixed(2) + ' KB'; + } else if (bytes > 1) { + bytes = bytes + ' bytes'; + } else if (bytes == 1) { + bytes = bytes + ' byte'; + } else { + bytes = ''; + } + return bytes; +} + +String.prototype.trim = function (char) { + if (char) { + return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), ''); + } + return this.replace(/^\s+|\s+$/g, ''); +}; + + +// README.md HEAD.md stand by +function markdown(el, data) { + if (window.md == undefined) { + //$.getScript('https://cdn.jsdelivr.net/npm/markdown-it@10.0.0/dist/markdown-it.min.js',function(){ + window.md = window.markdownit(); + markdown(el, data); + //}); + } else { + var html = md.render(data); + $(el).show().html(html); + } +} + +// Listen for fallback events +window.onpopstate = function () { + var path = window.location.pathname; + render(path); +} + + +$(function () { + init(); + var path = window.location.pathname; + /*$("body").on("click", '.folder', function () { + var url = $(this).attr('href'); + history.pushState(null, null, url); + render(url); + return false; + }); + $("body").on("click", '.view', function () { + var url = $(this).attr('href'); + history.pushState(null, null, url); + render(url); + return false; + });*/ + + render(path); +}); diff --git a/css/classic-button.css b/css/classic-button.css new file mode 100644 index 0000000..e0d1880 --- /dev/null +++ b/css/classic-button.css @@ -0,0 +1,24 @@ + a.button7{ + display:inline-block; + padding:0.7em 1.7em; + margin:0 0.3em 0.3em 0; + border-radius:0.2em; + box-sizing: border-box; + text-decoration:none; + font-family:'Roboto',sans-serif; + font-weight:400; + color:#FFFFFF; + background-color:#3369ff; + box-shadow:inset 0 -0.6em 1em -0.35em rgba(0,0,0,0.17),inset 0 0.6em 2em -0.3em rgba(255,255,255,0.15),inset 0 0 0em 0.05em rgba(255,255,255,0.12); + text-align:center; + position:relative; + } + a.button7:active{ + box-shadow:inset 0 0.6em 2em -0.3em rgba(0,0,0,0.15),inset 0 0 0em 0.05em rgba(255,255,255,0.12); + } + @media all and (max-width:30em){ + a.button7{ + display:block; + margin:0.4em auto; + } + } diff --git a/css/mail-box-style.css b/css/mail-box-style.css new file mode 100644 index 0000000..39f59ce --- /dev/null +++ b/css/mail-box-style.css @@ -0,0 +1,52 @@ +html > body { + /* override bootstrap background */ + background: #f9f9f9; +} + +footer p { + margin-top: 50px; + text-align: center; +} + +.card-block { + padding: 1.25rem; +} + +.email-table { + margin-top: 20px; +} + +div.min-height { + min-height: 400px; +} + +header { + background-color: #D9E2E9; + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 15px; +} + +#openRandomButton { + + margin-top: 6px; +} + +.email-table > .email { + border-top: 5px solid #7C96AB; + +} + +.header-shadow { + box-shadow: 0 2px 2px rgba(182, 182, 182, 0.75); +} + +.waiting-screen { + padding: 40px 15px; + text-align: center; +} + +.random-column { + border-left: 1px dashed #333; + +} diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 0000000..56def6d Binary files /dev/null and b/img/logo.png differ diff --git a/img/td-maker.png b/img/td-maker.png new file mode 100644 index 0000000..155216f Binary files /dev/null and b/img/td-maker.png differ diff --git a/maker.js b/maker.js new file mode 100644 index 0000000..e1ad251 --- /dev/null +++ b/maker.js @@ -0,0 +1,434 @@ +var authConfig = { + version: "1.0", + dailyLimit: true, // Whether to limit each mailbox to submit requests only once a day + client_id: '', // Google Client ID + client_secret: '', // Google Client Secret + refresh_token: '', // Refresh token + domain: "", //College name to display + black_list: ["example@gmail.com"] +}; + +var gd; + +var today; + +var html = ` + + + + Google Shared Drive + + + + + + + + + + + + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Google Logo

    ${ authConfig.domain ? ` +
    Create Shared Drive from: ${authConfig.domain}
    ` : "" } +

    + Multiple back-end API requests, the process takes a long time, please be patient! +
    + Never Submit Again, It'll ruin the System +
    + PS: One Team Drive request per day per Email-id +

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + Exit Source
    +
    +
    +
    +
    +
    +
    + + + + +
    + + +`; + +addEventListener("fetch", event => { + event.respondWith(handleRequest(event.request)); +}); + +var dailyLimit = []; + +/** + * Fetch and log a request + * @param {Request} request + */ +async function handleRequest(request) { + if (authConfig.dailyLimit) { + if (!today) today = new Date().getDate(); + + // Remove email rate limit every day + if (new Date().getDate() != today) { + today = new Date().getDate(); + dailyLimit.length = 0; + } + } + + if (gd == undefined) { + gd = new googleDrive(authConfig); + } + let url = new URL(request.url); + let path = url.pathname; + + switch (path) { + case "/teamDriveThemes": + let teamDriveThemes = await gd.getTeamDriveThemes(); + return new Response(JSON.stringify(teamDriveThemes), { + status: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*" + } + }); + case "/drive": + if (request.method === "POST") { + const requestBody = await request.json(); + + if (authConfig.dailyLimit) { + if (dailyLimit.includes(requestBody.emailAddress)) { + return new Response("Submit only one request per day.", { + status: 429 + }); + } else { + dailyLimit.push(requestBody.emailAddress); + } + } + + if (authConfig.black_list.includes(requestBody.emailAddress)) { + return new Response("Failed", { + status: 429 + }); + } + + try { + let result = await gd.createAndShareTeamDrive(requestBody); + return new Response("OK", { + status: 200 + }); + } catch (err) { + return new Response(err.toString(), { + status: 500 + }); + } + } else if (request.method === "OPTIONS") { + return new Response("", { + status: 200, + headers: { + "Access-Control-Allow-Origin": "*" + } + }); + } else { + return new Response("Bad Request", { + status: 400 + }); + } + default: + return new Response(html, { + status: 200, + headers: { + "Content-Type": "text/html; charset=utf-8", + "Access-Control-Allow-Origin": "*" + } + }); + } +} +// https://stackoverflow.com/a/2117523 +function uuidv4() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => { + // tslint:disable-next-line:one-variable-per-declaration + const r = (Math.random() * 16) | 0, + // tslint:disable-next-line:triple-equals + v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +class googleDrive { + constructor(authConfig) { + this.authConfig = authConfig; + this.accessToken(); + } + + async getTeamDriveThemes() { + let url = "https://www.googleapis.com/drive/v3/about"; + let requestOption = await this.requestOption(); + let params = { fields: "teamDriveThemes" }; + url += "?" + this.enQuery(params); + let response = await fetch(url, requestOption); + return await response.json(); + } + + async createAndShareTeamDrive(requestBody) { + // Create team drive + console.log("Creating TeamDrive"); + let url = "https://www.googleapis.com/drive/v3/drives"; + let requestOption = await this.requestOption( + { "Content-Type": "application/json" }, + "POST" + ); + let params = { requestId: uuidv4() }; + url += "?" + this.enQuery(params); + let post_data = { + name: requestBody.teamDriveName + }; + if ( + requestBody.teamDriveThemeId && + requestBody.teamDriveThemeId !== "random" + ) { + post_data.themeId = requestBody.teamDriveThemeId; + } + requestOption.body = JSON.stringify(post_data); + let response = await fetch(url, requestOption); + let result = await response.json(); + const teamDriveId = result.id; + console.log("Created TeamDrive ID", teamDriveId); + + // Get created drive user permission ID + console.log(`Getting creator permission ID`); + url = `https://www.googleapis.com/drive/v3/files/${teamDriveId}/permissions`; + params = { supportsAllDrives: true }; + params.fields = "permissions(id,emailAddress)"; + url += "?" + this.enQuery(params); + requestOption = await this.requestOption(); + response = await fetch(url, requestOption); + result = await response.json(); + const currentUserPermissionID = result.permissions[0].id; + console.log(currentUserPermissionID); + + // Share team drive with email address + console.log(`Sharing the team drive to ${requestBody.emailAddress}`); + url = `https://www.googleapis.com/drive/v3/files/${teamDriveId}/permissions`; + params = { supportsAllDrives: true }; + url += "?" + this.enQuery(params); + requestOption = await this.requestOption( + { "Content-Type": "application/json" }, + "POST" + ); + post_data = { + role: "organizer", + type: "user", + emailAddress: requestBody.emailAddress + }; + requestOption.body = JSON.stringify(post_data); + response = await fetch(url, requestOption); + await response.json(); + + // Delete creator from the team drive + console.log("Deleting creator from the team drive"); + url = `https://www.googleapis.com/drive/v3/files/${teamDriveId}/permissions/${currentUserPermissionID}`; + params = { supportsAllDrives: true }; + url += "?" + this.enQuery(params); + requestOption = await this.requestOption({}, "DELETE"); + response = await fetch(url, requestOption); + return await response.text(); + } + + 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 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("&"); + } +}