This commit is contained in:
Simon Bruder 2020-05-21 00:16:06 +02:00
commit 8303ec0e16
No known key found for this signature in database
GPG key ID: 6F03E0000CC5B62F
10 changed files with 3341 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
dist

24
LICENSE Normal file
View file

@ -0,0 +1,24 @@
Based on https://github.com/maple3142/GDIndex
MIT License
Copyright (c) 2019-2020 maple3142, Simon Bruder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

10
bili.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
input: 'index.js',
output: {
dir: 'dist',
fileName: 'worker.js',
format: 'iife'
},
minify: false,
target: 'browser'
}

171
googleDrive.js Normal file
View file

@ -0,0 +1,171 @@
import xf from './xfetch'
class GoogleDrive {
constructor(auth) {
this.auth = auth
this.expires = 0
this._getIdCache = new Map()
}
async initializeClient() {
// any method that do api call must call this beforehand
if (Date.now() < this.expires) return
const resp = await xf
.post('https://www.googleapis.com/oauth2/v4/token', {
urlencoded: {
client_id: this.auth.client_id,
client_secret: this.auth.client_secret,
refresh_token: this.auth.refresh_token,
grant_type: 'refresh_token'
}
})
.json()
this.client = xf.extend({
baseURI: 'https://www.googleapis.com/drive/v3/',
headers: {
Authorization: `Bearer ${resp.access_token}`
}
})
this.expires = Date.now() + 3500 * 1000 // normally, it should expiers after 3600 seconds
}
async listDrive() {
await this.initializeClient()
return this.client.get('drives').json()
}
async download(id, range = '') {
await this.initializeClient()
return this.client.get(`files/${id}`, {
qs: {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
alt: 'media'
},
headers: {
Range: range
}
})
}
async downloadByPath(path, rootId = 'root', range = '') {
const id = await this.getId(path, rootId)
if (!id) return null
return this.download(id, range)
}
async getMeta(id) {
await this.initializeClient()
return this.client
.get(`files/${id}`, {
qs: {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
fields: '*'
}
})
.json()
}
async getMetaByPath(path, rootId = 'root') {
const id = await this.getId(path, rootId)
if (!id) return null
return this.getMeta(id)
}
async listFolder(id) {
await this.initializeClient()
const getList = pageToken => {
const qs = {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
q: `'${id}' in parents and trashed = false`,
orderBy: 'folder,name,modifiedTime desc',
fields:
'files(id,name,mimeType,size,modifiedTime),nextPageToken',
pageSize: 1000
}
if (pageToken) {
qs.pageToken = pageToken
}
return this.client
.get('files', {
qs
})
.json()
}
const files = []
let pageToken
do {
const resp = await getList(pageToken)
files.push(...resp.files)
pageToken = resp.nextPageToken
} while (pageToken)
return { files }
}
async listFolderByPath(path, rootId = 'root') {
const id = await this.getId(path, rootId)
if (!id) return null
return this.listFolder(id)
}
async getId(path, rootId = 'root') {
const toks = path.split('/').filter(Boolean)
let id = rootId
for (const tok of toks) {
id = await this._getId(id, tok)
}
return id
}
async _getId(parentId, childName) {
if (this._getIdCache.has(parentId + childName)) {
return this._getIdCache.get(parentId + childName)
}
await this.initializeClient()
childName = childName.replace(/\'/g, `\\'`) // escape single quote
const resp = await this.client
.get('files', {
qs: {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
q: `'${parentId}' in parents and name = '${childName}' and trashed = false`,
fields: 'files(id)'
}
})
.json()
.catch(e => ({ files: [] })) // if error, make it empty
if (resp.files.length === 0) {
return null
}
this._getIdCache.has(parentId + childName)
return resp.files[0].id // when there are more than 1 items, simply return the first one
}
async upload(parentId, name, file) {
await this.initializeClient()
const createResp = await this.client.post(
'https://www.googleapis.com/upload/drive/v3/files',
{
qs: {
uploadType: 'resumable',
supportsAllDrives: true
},
json: {
name,
parents: [parentId]
}
}
)
const putUrl = createResp.headers.get('Location')
return this.client
.put(putUrl, {
body: file
})
.json()
}
async uploadByPath(path, name, file, rootId = 'root') {
const id = await this.getId(path, rootId)
if (!id) return null
return this.upload(id, name, file)
}
async delete(fileId) {
return this.client.delete(`files/${fileId}`)
}
async deleteByPath(path, rootId = 'root') {
const id = await this.getId(path, rootId)
if (!id) return null
return this.delete(id)
}
}
export default GoogleDrive

97
index.js Normal file
View file

@ -0,0 +1,97 @@
// vim: set noexpandtab:
import mime from 'mime'
import GoogleDrive from './googleDrive'
self.props = {
root_id: typeof(ROOT_ID) !== 'undefined' ? ROOT_ID : 'root',
client_id: typeof(CLIENT_ID) !== 'undefined' ? CLIENT_ID : '202264815644.apps.googleusercontent.com',
client_secret: typeof(CLIENT_SECRET) !== 'undefined' ? CLIENT_SECRET : 'X4Z3ca8xfWDb1Voo-F9a7ZxJ',
refresh_token: typeof(REFRESH_TOKEN) !== 'undefined' ? REFRESH_TOKEN : '' // left here for debugging
}
const gd = new GoogleDrive(self.props)
async function onGet(request) {
let { pathname: path } = request
const rootId =
request.searchParams.get('rootId') || self.props.root_id
const result = await gd.getMetaByPath(path, rootId)
if (!result || result.mimeType.includes('vnd.google-apps')) {
return new Response('404 Not Found', {
headers: {
'Content-Type': 'text/plain'
},
status: 404
})
}
const r = await gd.download(result.id, request.headers.get('Range'))
const h = new Headers(r.headers)
h.set(
'Content-Disposition',
`inline; filename*=UTF-8''${encodeURIComponent(result.name)}`
)
return new Response(r.body, {
status: r.status,
headers: h
})
}
function encodePathComponent(path) {
return path
.split('/')
.map(encodeURIComponent)
.join('/')
}
async function handleRequest(request) {
if (request.method === 'OPTIONS')
// allow preflight request
return new Response('', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS'
}
})
request = Object.assign({}, request, new URL(request.url))
request.pathname = request.pathname
.split('/')
.map(decodeURIComponent)
.map(decodeURIComponent) // for some super special cases, browser will force encode it... eg: +αあるふぁきゅん。 - +♂.mp3
.join('/')
if (request.pathname.endsWith('/')) request.pathname += 'index.html'
let resp
if (request.method === 'GET') resp = await onGet(request)
else
resp = new Response('', {
status: 405
})
const obj = Object.create(null)
for (const [k, v] of resp.headers.entries()) {
obj[k] = v
}
return new Response(resp.body, {
status: resp.status,
statusText: resp.statusText,
headers: Object.assign(obj, {
'Access-Control-Allow-Origin': '*'
})
})
}
addEventListener('fetch', event => {
event.respondWith(
handleRequest(event.request).catch(err => {
console.error(err)
new Response(JSON.stringify(err.stack), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
})
})
)
})

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "gdindex-worker",
"version": "0.1.0",
"main": "dist/worker.js",
"license": "MIT",
"dependencies": {
"mime": "^2.4.4",
"path-to-regexp": "^3.1.0"
},
"devDependencies": {
"bili": "^4.8.1"
},
"scripts": {
"build": "bili"
}
}

75
router.js Normal file
View file

@ -0,0 +1,75 @@
const pathToRegexp = require('path-to-regexp')
class Router {
constructor() {
this.handlers = []
}
use(handler) {
this.handlers.push(handler)
return this
}
useRoute(path, handler) {
const keys = []
const re = pathToRegexp(path, keys)
this.use(async (req, res, next) => {
if (re.test(req.pathname)) {
const [_, ...result] = re.exec(req.pathname)
const params = {}
for (let i = 0; i < result.length; i++) {
params[keys[i].name] = result[i]
}
req.params = params
await handler(req, res, next)
}
})
return this
}
useRouteWithVerb(verb, path, handler) {
verb = verb.toUpperCase()
this.useRoute(path, async (req, res, next) => {
if (req.method === verb) {
await handler(req, res, next)
}
})
return this
}
useWithVerb(verb, handler) {
verb = verb.toUpperCase()
this.use(async (req, res, next) => {
if (req.method === verb) {
await handler(req, res, next)
}
})
return this
}
async handle(request) {
const responseCtx = {
body: '',
headers: {}
}
const requestCtx = Object.assign({}, request, new URL(request.url))
const createNext = n => async () => {
const fn = this.handlers[n]
if (!fn) return
let gotCalled = false
const next = createNext(n + 1)
await fn(requestCtx, responseCtx, () => {
gotCalled = true
return next()
})
if (!gotCalled) {
return next()
}
}
await createNext(0)()
return responseCtx.response ? responseCtx.response : new Response(responseCtx.body, responseCtx)
}
}
for (const verb of ['get', 'options', 'head']) {
Router.prototype[verb] = function(path, handler) {
if (handler) this.useRouteWithVerb(verb, path, handler)
else this.useWithVerb(verb, path) // when there is only 1 argument, path is handler
return this
}
}
module.exports = Router

4
wrangler.toml Normal file
View file

@ -0,0 +1,4 @@
name = "gdcdn"
type = "javascript"
account_id = "8f78257f5eee59cdae86b89edc91082f"
workers_dev = "true"

116
xfetch.js Normal file
View file

@ -0,0 +1,116 @@
/*
* XFetch.js modified
* A extremely simple fetch extension inspired by sindresorhus/ky.
*/
const xf = (() => {
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head']
class HTTPError extends Error {
constructor(res) {
super(res.statusText)
this.name = 'HTTPError'
this.response = res
}
}
class XResponsePromise extends Promise {}
for (const alias of ['arrayBuffer', 'blob', 'formData', 'json', 'text']) {
// alias for .json() .text() etc...
XResponsePromise.prototype[alias] = function(fn) {
return this.then(res => res[alias]()).then(fn || (x => x))
}
}
const { assign } = Object
function mergeDeep(target, source) {
const isObject = obj => obj && typeof obj === 'object'
if (!isObject(target) || !isObject(source)) {
return source
}
Object.keys(source).forEach(key => {
const targetValue = target[key]
const sourceValue = source[key]
if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
target[key] = targetValue.concat(sourceValue)
} else if (isObject(targetValue) && isObject(sourceValue)) {
target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue)
} else {
target[key] = sourceValue
}
})
return target
}
const fromEntries = ent => ent.reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
const typeis = (...types) => val =>
types.some(type => (typeof type === 'string' ? typeof val === type : val instanceof type))
const isstr = typeis('string')
const isobj = typeis('object')
const isstrorobj = v => isstr(v) || isobj(v)
const responseErrorThrower = res => {
if (!res.ok) throw new HTTPError(res)
return res
}
const extend = (defaultInit = {}) => {
const xfetch = (input, init = {}) => {
mergeDeep(init, defaultInit)
const createQueryString = o => new init.URLSearchParams(o).toString()
const parseQueryString = s => fromEntries([...new init.URLSearchParams(s).entries()])
const url = new init.URL(input, init.baseURI || undefined)
if (!init.headers) {
init.headers = {}
} else if (typeis(init.Headers)(init.headers)) {
// Transform into object if it is `Headers`
init.headers = fromEntries([...init.headers.entries()])
}
// Add json or form on body
if (init.json) {
init.body = JSON.stringify(init.json)
init.headers['Content-Type'] = 'application/json'
} else if (isstrorobj(init.urlencoded)) {
init.body = isstr(init.urlencoded) ? init.urlencoded : createQueryString(init.urlencoded)
init.headers['Content-Type'] = 'application/x-www-form-urlencoded'
} else if (typeis(init.FormData, 'object')(init.formData)) {
// init.formData is data passed by user, init.FormData is FormData constructor
if (!typeis(init.FormData)(init.formData)) {
const fd = new init.FormData()
for (const [k, v] of Object.entries(init.formData)) {
fd.append(k, v)
}
init.formData = fd
}
init.body = init.formData
}
// Querystring
if (init.qs) {
if (isstr(init.qs)) init.qs = parseQueryString(init.qs)
url.search = createQueryString(assign(fromEntries([...url.searchParams.entries()]), init.qs))
}
return XResponsePromise.resolve(init.fetch(url, init).then(responseErrorThrower))
}
for (const method of METHODS) {
xfetch[method] = (input, init = {}) => {
init.method = method.toUpperCase()
return xfetch(input, init)
}
}
// Extra methods and classes
xfetch.extend = newDefaultInit => extend(assign({}, defaultInit, newDefaultInit))
xfetch.HTTPError = HTTPError
return xfetch
}
const isWindow = typeof document !== 'undefined'
const isBrowser = typeof self !== 'undefined' // works in both window & worker scope
return isBrowser
? extend({
fetch: fetch.bind(self),
URL,
Response,
URLSearchParams,
Headers,
FormData,
baseURI: isWindow ? document.baseURI : '' // since there is no document in webworkers
})
: extend()
})()
export default xf

2826
yarn.lock Normal file

File diff suppressed because it is too large Load diff