├── LICENSE ├── README.md └── gdrive.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 thim0o 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gdrive-cfworker-videostream 2 | A cloudflare worker that provides direct links to files on google drive. 3 |
4 | The worker can search on all your drives (including shared drives), and returns the search results in plaintext or in json format. 5 | 6 | 7 | 8 |

Setup

9 | Follow the instructions on this page for an easy setup 10 | https://gdrive-cfworker.glitch.me/ 11 | 12 |

Usage

13 | After you create the cloudflare worker, take note of its url which looks like https://x.y.workers.dev: 14 | You can change this url on the cloudflare workers page, but make sure it's non-guessable. 15 | 16 | The requests you can make to this url are: 17 | 18 | 19 | | GET request | Response | 20 | | ------------- | ------------- | 21 | | x.y.workers.dev/search/*SomeSearchQuery* | Search results on an html page | 22 | | x.y.workers.dev/searchjson/*SomeSearchQuery* | Search results in json format | 23 | 24 | Credits go to https://github.com/maple3142/GDIndex for large parts of the code 25 | -------------------------------------------------------------------------------- /gdrive.js: -------------------------------------------------------------------------------- 1 | var authConfig = { 2 | "client_id": "202264815644.apps.googleusercontent.com", // RClone client_id 3 | "client_secret": "X4Z3ca8xfWDb1Voo-F9a7ZxJ", // RClone client_secret 4 | "refresh_token": "", // unique 5 | "root": "allDrives" 6 | }; 7 | 8 | 9 | let gd; 10 | 11 | 12 | addEventListener('fetch', event => { 13 | event.respondWith(handleRequest(event.request)); 14 | }); 15 | 16 | 17 | /** 18 | * Fetch and log a request 19 | * @param {Request} request 20 | */ 21 | async function handleRequest(request) { 22 | let linksHtml = ` 23 | 24 | 25 | 26 | Links 27 | 28 | SEARCH_RESULT_PLACEHOLDER 29 | 30 | 31 | `; 32 | 33 | if (gd === undefined) { 34 | gd = new googleDrive(authConfig); 35 | } 36 | 37 | let url = new URL(request.url); 38 | let path = url.pathname; 39 | let action = url.searchParams.get('a'); 40 | 41 | 42 | if (path.substr(-1) === '/' || action != null) { 43 | return new Response(linksHtml, {status: 200, headers: {'Content-Type': 'text/html; charset=utf-8'}}); 44 | } else { 45 | let baseUrl = url.toString().replace(path, "/"); 46 | 47 | // If client requests a search 48 | if (path.startsWith("/search/")) { 49 | let file = await gd.getFilesCached(path); 50 | if (file !== undefined) { 51 | file.forEach(function (f) { 52 | let link = baseUrl + encodeURIComponent(f.name); 53 | linksHtml = linksHtml.replace("SEARCH_RESULT_PLACEHOLDER", `${f.name} ${f.size}

\nSEARCH_RESULT_PLACEHOLDER`); 54 | }); 55 | } else { 56 | return new Response("Could not load the search results", { 57 | status: 500, 58 | headers: {'Content-Type': 'text/html; charset=utf-8'} 59 | }); 60 | } 61 | 62 | linksHtml = linksHtml.replace("SEARCH_RESULT_PLACEHOLDER", ""); 63 | return new Response(linksHtml, {status: 200, headers: {'Content-Type': 'text/html; charset=utf-8'}}); 64 | 65 | 66 | } else if (path.startsWith("/searchjson/")) { 67 | const response = []; 68 | let file = await gd.getFilesCached(path); 69 | if (file !== undefined) { 70 | file.forEach(function (f) { 71 | let link = baseUrl + encodeURIComponent(f.name); 72 | let size = Math.round(f.size / 2 ** 30 * 100) / 100; 73 | let result = {"link": link, "size_gb": size, "file_id": f.id, "drive_id": f.driveId}; 74 | response.push(result); 75 | }); 76 | } 77 | console.log(response) 78 | return new Response(JSON.stringify(response), {status: 200, headers: {'Content-Type': 'application/json'}}); 79 | 80 | } else { 81 | // If client requests a file 82 | let file = await gd.getFilesCached(path); 83 | file = file[0]; 84 | console.log(file); 85 | let range = request.headers.get('Range'); 86 | return gd.down(file.id, range); 87 | } 88 | } 89 | } 90 | 91 | 92 | class googleDrive { 93 | constructor(authConfig) { 94 | this.authConfig = authConfig; 95 | this.paths = []; 96 | this.files = []; 97 | this.paths["/"] = authConfig.root; 98 | this.accessToken(); 99 | } 100 | 101 | async fetchAndRetryOnError(url, requestOption, maxRetries = 3) { 102 | let response = await fetch(url, requestOption); 103 | let retries = 0 104 | while (!response.ok && response.status != 400 && retries < maxRetries) { 105 | console.log(response.status); 106 | await sleep(1000 + 1000 * 2 ** retries) 107 | retries += 1 108 | response = await fetch(url, requestOption); 109 | console.log(`Retry nr. ${retries} result: ${response.ok}`) 110 | } 111 | return response 112 | } 113 | 114 | async down(id, range = '') { 115 | let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`; 116 | let requestOption = await this.requestOption(); 117 | requestOption.headers['Range'] = range; 118 | return await this.fetchAndRetryOnError(url, requestOption); 119 | } 120 | 121 | async getFilesCached(path) { 122 | if (typeof this.files[path] == 'undefined') { 123 | let files = await this.getFiles(path) 124 | if (files !== "FAILED") { 125 | this.files[path] = files; 126 | } 127 | 128 | } 129 | return this.files[path]; 130 | } 131 | 132 | getSearchScopeParams() { 133 | let params = {'spaces': 'drive'}; 134 | if (authConfig.root === "allDrives") { 135 | params = { 136 | 'corpora': 'allDrives', 137 | 'includeItemsFromAllDrives': true, 138 | 'supportsAllDrives': true, 139 | 'pageSize': 1000 140 | }; 141 | 142 | } else if (authConfig.root !== "") { 143 | params = { 144 | 'spaces': 'drive', 145 | 'corpora': 'drive', 146 | 'includeItemsFromAllDrives': true, 147 | 'supportsAllDrives': true, 148 | 'driveId': authConfig.root 149 | }; 150 | } 151 | return params 152 | } 153 | 154 | async getFiles(path) { 155 | let arr = path.split('/'); 156 | let name = arr.pop(); 157 | name = decodeURIComponent(name).replace(/'/g, "\\'"); 158 | console.log(name); 159 | 160 | let url = 'https://www.googleapis.com/drive/v3/files'; 161 | let params = this.getSearchScopeParams() 162 | 163 | params.q = `fullText contains '${name}' and (mimeType contains 'application/octet-stream' or mimeType contains 'video/') and (name contains 'mkv' or name contains 'mp4' or name contains 'avi') `; 164 | params.fields = "files(id, name, size, driveId)"; 165 | 166 | url += '?' + this.enQuery(params); 167 | let requestOption = await this.requestOption(); 168 | let response = await this.fetchAndRetryOnError(url, requestOption); 169 | 170 | let obj = await response.json(); 171 | console.log(obj); 172 | console.log(obj.files); 173 | 174 | if (response.ok) { 175 | return obj.files; 176 | } else { 177 | return "FAILED" 178 | } 179 | } 180 | 181 | 182 | async accessToken() { 183 | console.log("accessToken"); 184 | if (this.authConfig.expires === undefined || this.authConfig.expires < Date.now()) { 185 | const obj = await this.fetchAccessToken(); 186 | if (obj.access_token !== undefined) { 187 | this.authConfig.accessToken = obj.access_token; 188 | this.authConfig.expires = Date.now() + 3500 * 1000; 189 | } 190 | } 191 | return this.authConfig.accessToken; 192 | } 193 | 194 | async fetchAccessToken() { 195 | console.log("fetchAccessToken"); 196 | const url = "https://www.googleapis.com/oauth2/v4/token"; 197 | const headers = { 198 | 'Content-Type': 'application/x-www-form-urlencoded' 199 | }; 200 | const post_data = { 201 | 'client_id': this.authConfig.client_id, 202 | 'client_secret': this.authConfig.client_secret, 203 | 'refresh_token': this.authConfig.refresh_token, 204 | 'grant_type': 'refresh_token' 205 | }; 206 | 207 | let requestOption = { 208 | 'method': 'POST', 209 | 'headers': headers, 210 | 'body': this.enQuery(post_data) 211 | }; 212 | 213 | const response = await this.fetchAndRetryOnError(url, requestOption); 214 | return await response.json(); 215 | } 216 | 217 | 218 | async requestOption(headers = {}, method = 'GET') { 219 | const accessToken = await this.accessToken(); 220 | headers['authorization'] = 'Bearer ' + accessToken; 221 | return {'method': method, 'headers': headers}; 222 | } 223 | 224 | enQuery(data) { 225 | const ret = []; 226 | for (let d in data) { 227 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])); 228 | } 229 | ret.push(encodeURIComponent("acknowledgeAbuse") + '=' + encodeURIComponent("true")); 230 | return ret.join('&'); 231 | } 232 | } 233 | 234 | function sleep(ms) { 235 | return new Promise(resolve => setTimeout(resolve, ms)); 236 | } 237 | --------------------------------------------------------------------------------