├── 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 |
--------------------------------------------------------------------------------