├── assets ├── got-code.png ├── client-id.png ├── screenshot.png ├── register-app.png ├── add-permissions.png ├── private-folder.png ├── add-client-secret.png ├── authorize-for-code.png ├── get-access-token.png ├── onedrive-cf-index.png ├── permissions-used.png ├── chinese.svg └── english.svg ├── .prettierrc ├── .gitignore ├── wrangler.toml ├── .github ├── workflows │ └── main.yml └── stale.yml ├── .eslintrc.js ├── src ├── auth │ ├── config.js │ ├── credentials.js │ └── onedrive.js ├── render │ ├── pathUtil.js │ ├── fileExtension.js │ ├── mdRenderer.js │ ├── htmlWrapper.js │ └── favicon.js ├── files │ └── load.js ├── config │ └── default.js ├── folderView.js ├── index.js └── fileView.js ├── package.json ├── LICENSE ├── themes ├── prism-github.css └── spencer.css ├── CODE_OF_CONDUCT.md ├── README-CN.md └── README.md /assets/got-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/got-code.png -------------------------------------------------------------------------------- /assets/client-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/client-id.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /assets/register-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/register-app.png -------------------------------------------------------------------------------- /assets/add-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/add-permissions.png -------------------------------------------------------------------------------- /assets/private-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/private-folder.png -------------------------------------------------------------------------------- /assets/add-client-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/add-client-secret.png -------------------------------------------------------------------------------- /assets/authorize-for-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/authorize-for-code.png -------------------------------------------------------------------------------- /assets/get-access-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/get-access-token.png -------------------------------------------------------------------------------- /assets/onedrive-cf-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/onedrive-cf-index.png -------------------------------------------------------------------------------- /assets/permissions-used.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spencerwooo/onedrive-cf-index/HEAD/assets/permissions-used.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "tabWidth": 2, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | **/*.rs.bk 4 | Cargo.lock 5 | bin/ 6 | pkg/ 7 | wasm-pack.log 8 | worker/ 9 | node_modules/ 10 | .cargo-ok 11 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "drive" 2 | type = "webpack" 3 | workers_dev = true 4 | account_id = "b87997a496f985b46b927b3f24c61de1" 5 | zone_id = "b04152d2c33ff4653cec1a579be6d42a" 6 | kv_namespaces = [ 7 | { binding = "BUCKET", preview_id = "55b0fc4e288a465fb225421f2af5d445", id = "09b0cf433f394b309d35d1ff3bd0bf39" } 8 | ] 9 | 10 | [env.staging] 11 | name = "drive-staging" 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | name: Deploy to Cloudflare Workers 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Publish 15 | uses: cloudflare/wrangler-action@1.3.0 16 | with: 17 | apiToken: ${{ secrets.CF_API_TOKEN }} 18 | wranglerVersion: '1.13.0' 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true 5 | }, 6 | extends: ['standard'], 7 | parserOptions: { 8 | ecmaVersion: 11, 9 | sourceType: 'module' 10 | }, 11 | rules: { 12 | 'space-before-function-paren': ['error', 'never'] 13 | }, 14 | globals: { 15 | TransformStream: true, 16 | REFRESH_TOKEN: true, 17 | CLIENT_SECRET: true, 18 | BUCKET: true, 19 | AUTH_PASSWORD: true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 7 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 1 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - todo 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /src/auth/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic authentication. 3 | * Enabled by default, you need to set PASSWORD secret using `wrangler secret put AUTH_PASSWORD` 4 | * 5 | * AUTH_ENABLED `false` to disable it 6 | * NAME user name 7 | * ENABLE_PATHS enable protection on specific folders/files 8 | */ 9 | export const AUTH_ENABLED = true 10 | export const NAME = 'guest' 11 | export const ENABLE_PATHS = ['/🌞 Private folder/Private folder'] 12 | 13 | /** 14 | * RegExp for basic auth credentials 15 | * 16 | * credentials = auth-scheme 1*SP token68 17 | * auth-scheme = "Basic" ; case insensitive 18 | * token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" 19 | */ 20 | 21 | export const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/ 22 | 23 | /** 24 | * RegExp for basic auth user/pass 25 | * 26 | * user-pass = userid ":" password 27 | * userid = * 28 | * password = *TEXT 29 | */ 30 | 31 | export const USER_PASS_REGEXP = /^([^:]*):(.*)$/ 32 | -------------------------------------------------------------------------------- /src/render/pathUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return absolute path of current working directory 3 | * 4 | * @param {array} pathItems List of current directory items 5 | * @param {number} idx Current depth inside home 6 | */ 7 | function getPathLink(pathItems, idx) { 8 | const pathList = pathItems.slice(0, idx + 1) 9 | 10 | if (pathList.length === 1) { 11 | return '/' 12 | } 13 | 14 | pathList[0] = '' 15 | return pathList.join('/') + '/' 16 | } 17 | 18 | /** 19 | * Render directory breadcrumb 20 | * 21 | * @param {string} path current working directory, for instance: /🥑 Course PPT for CS (BIT)/2018 - 大三上 - 操作系统/ 22 | */ 23 | export function renderPath(path) { 24 | const pathItems = path.split('/') 25 | pathItems[0] = '/' 26 | pathItems.pop() 27 | 28 | const link = (href, content) => `${content}` 29 | const breadcrumb = [] 30 | pathItems.forEach((item, idx) => { 31 | breadcrumb.push(link(getPathLink(pathItems, idx), idx === 0 ? '🚩 Home' : decodeURIComponent(item))) 32 | }) 33 | 34 | return breadcrumb.join(' / ') 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "onedrive-cf-index", 4 | "version": "1.0.0", 5 | "description": "A template for kick starting a Cloudflare Workers project", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "format": "prettier --write '**/*.{js,css,json,md}'", 10 | "lint": "eslint src/**/* *.js --color --fix", 11 | "preview": "wrangler preview --watch", 12 | "dev": "wrangler dev" 13 | }, 14 | "author": "spencerwooo ", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "eslint": "^7.6.0", 18 | "eslint-config-prettier": "^6.11.0", 19 | "eslint-config-standard": "^14.1.1", 20 | "eslint-plugin-import": "^2.22.0", 21 | "eslint-plugin-node": "^11.1.0", 22 | "eslint-plugin-prettier": "^3.1.4", 23 | "eslint-plugin-promise": "^4.2.1", 24 | "eslint-plugin-standard": "^4.0.1", 25 | "prettier": "^1.18.2" 26 | }, 27 | "dependencies": { 28 | "emoji-regex": "^9.2.0", 29 | "font-awesome-filetypes": "^2.1.0", 30 | "marked": "^2.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/auth/credentials.js: -------------------------------------------------------------------------------- 1 | import { CREDENTIALS_REGEXP, USER_PASS_REGEXP } from './config' 2 | 3 | /** 4 | * Object to represent user credentials. 5 | */ 6 | class Credentials { 7 | constructor(name, pass) { 8 | this.name = name 9 | this.pass = pass 10 | } 11 | } 12 | 13 | /** 14 | * Parse basic auth to object. 15 | */ 16 | export function parseAuthHeader(string) { 17 | if (typeof string !== 'string') { 18 | return undefined 19 | } 20 | 21 | // parse header 22 | const match = CREDENTIALS_REGEXP.exec(string) 23 | 24 | if (!match) { 25 | return undefined 26 | } 27 | 28 | // decode user pass 29 | const userPass = USER_PASS_REGEXP.exec(atob(match[1])) 30 | 31 | if (!userPass) { 32 | return undefined 33 | } 34 | 35 | // return credentials object 36 | return new Credentials(userPass[1], userPass[2]) 37 | } 38 | 39 | export function unauthorizedResponse(body) { 40 | return new Response(null, { 41 | status: 401, 42 | statusText: "'Authentication required.'", 43 | body: body, 44 | headers: { 45 | 'WWW-Authenticate': 'Basic realm="User Visible Realm"' 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Ashley Williams 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /src/render/fileExtension.js: -------------------------------------------------------------------------------- 1 | const preview = { 2 | markdown: 'markdown', 3 | image: 'image', 4 | text: 'text', 5 | pdf: 'pdf', 6 | code: 'code', 7 | video: 'video', 8 | audio: 'audio' 9 | } 10 | 11 | const extensions = { 12 | gif: preview.image, 13 | jpeg: preview.image, 14 | jpg: preview.image, 15 | png: preview.image, 16 | 17 | md: preview.markdown, 18 | markdown: preview.markdown, 19 | mdown: preview.markdown, 20 | 21 | pdf: preview.pdf, 22 | 23 | c: preview.code, 24 | cpp: preview.code, 25 | js: preview.code, 26 | java: preview.code, 27 | sh: preview.code, 28 | cs: preview.code, 29 | py: preview.code, 30 | css: preview.code, 31 | html: preview.code, 32 | ts: preview.code, 33 | vue: preview.code, 34 | json: preview.code, 35 | yaml: preview.code, 36 | toml: preview.code, 37 | 38 | txt: preview.text, 39 | 40 | mp4: preview.video, 41 | flv: preview.video, 42 | webm: preview.video, 43 | m3u8: preview.video, 44 | mkv: preview.video, 45 | 46 | mp3: preview.audio, 47 | m4a: preview.audio, 48 | aac: preview.audio, 49 | wav: preview.audio, 50 | ogg: preview.audio, 51 | oga: preview.audio, 52 | opus: preview.audio, 53 | flac: preview.audio 54 | } 55 | 56 | export { extensions, preview } 57 | -------------------------------------------------------------------------------- /src/render/mdRenderer.js: -------------------------------------------------------------------------------- 1 | import marked from 'marked' 2 | import { cleanUrl } from 'marked/src/helpers' 3 | 4 | // Rewrite renderer, see original at: https://github.com/markedjs/marked/blob/master/src/Renderer.js 5 | const renderer = new marked.Renderer() 6 | renderer.image = (href, title, text) => { 7 | href = cleanUrl(false, null, href) 8 | if (href === null) { 9 | return text 10 | } 11 | let url 12 | try { 13 | // Check if href is relative 14 | url = new URL(href).href 15 | } catch (TypeError) { 16 | // Add param raw 17 | if (href.includes('?')) { 18 | const urlSplitParams = href.split('?') 19 | const param = new URLSearchParams(urlSplitParams[1]) 20 | param.append('raw') 21 | url = urlSplitParams[0] + '?' + param.toString() 22 | } else { 23 | url = href + '?raw' 24 | } 25 | } 26 | let out = '' + text + ' 47 | ${renderedMd} 48 | ` 49 | } 50 | -------------------------------------------------------------------------------- /themes/prism-github.css: -------------------------------------------------------------------------------- 1 | code, 2 | code[class*='language-'], 3 | pre[class*='language-'] { 4 | color: #24292e; 5 | text-align: left; 6 | white-space: pre; 7 | word-spacing: normal; 8 | tab-size: 4; 9 | hyphens: none; 10 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 11 | line-height: 1.4; 12 | direction: ltr; 13 | cursor: text; 14 | } 15 | 16 | pre[class*='language-'] { 17 | overflow: auto; 18 | margin: 1em 0; 19 | padding: 1.2em; 20 | border-radius: 3px; 21 | font-size: 85%; 22 | } 23 | 24 | p code, 25 | li code, 26 | table code { 27 | margin: 0; 28 | border-radius: 3px; 29 | padding: 0.2em 0; 30 | font-size: 85%; 31 | } 32 | p code:before, 33 | p code:after, 34 | li code:before, 35 | li code:after, 36 | table code:before, 37 | table code:after { 38 | letter-spacing: -0.2em; 39 | content: '\00a0'; 40 | } 41 | 42 | :not(pre) > code[class*='language-'] { 43 | padding: 0.1em; 44 | border-radius: 0.3em; 45 | } 46 | 47 | .token.comment, 48 | .token.prolog, 49 | .token.doctype, 50 | .token.cdata { 51 | color: #6a737d; 52 | } 53 | .token.punctuation, 54 | .token.string, 55 | .token.atrule, 56 | .token.attr-value { 57 | color: #032f62; 58 | } 59 | .token.property, 60 | .token.tag { 61 | color: #22863a; 62 | } 63 | .token.boolean, 64 | .token.number { 65 | color: #005cc5; 66 | } 67 | .token.selector, 68 | .token.attr-name, 69 | .token.attr-value .punctuation:first-child, 70 | .token.keyword, 71 | .token.regex, 72 | .token.important { 73 | color: #d73a49; 74 | } 75 | .token.operator, 76 | .token.entity, 77 | .token.url, 78 | .language-css .token.string { 79 | color: #005cc5; 80 | } 81 | .token.entity { 82 | cursor: help; 83 | } 84 | 85 | .namespace { 86 | opacity: 0.7; 87 | } 88 | -------------------------------------------------------------------------------- /src/auth/onedrive.js: -------------------------------------------------------------------------------- 1 | import config from '../config/default' 2 | 3 | /** 4 | * Get access token for microsoft graph API endpoints. Refresh token if needed. 5 | */ 6 | export async function getAccessToken() { 7 | const timestamp = () => { 8 | return Math.floor(Date.now() / 1000) 9 | } 10 | 11 | // Fetch access token 12 | const data = await BUCKET.get('onedrive', 'json') 13 | if (data && data.access_token && timestamp() < data.expire_at) { 14 | console.log('Fetched token from storage.') 15 | return data.access_token 16 | } 17 | 18 | // Token expired, refresh access token with Microsoft API. Both international and china-specific API are supported 19 | const oneDriveAuthEndpoint = `${config.apiEndpoint.auth}/token` 20 | 21 | const resp = await fetch(oneDriveAuthEndpoint, { 22 | method: 'POST', 23 | body: `client_id=${config.client_id}&redirect_uri=${config.redirect_uri}&client_secret=${config.client_secret} 24 | &refresh_token=${config.refresh_token}&grant_type=refresh_token`, 25 | headers: { 26 | 'Content-Type': 'application/x-www-form-urlencoded' 27 | } 28 | }) 29 | if (resp.ok) { 30 | console.info('Successfully refreshed access_token.') 31 | const data = await resp.json() 32 | 33 | // Update expiration time on token refresh 34 | data.expire_at = timestamp() + data.expires_in 35 | 36 | await BUCKET.put('onedrive', JSON.stringify(data)) 37 | console.info('Successfully updated access_token.') 38 | 39 | // Finally, return access token 40 | return data.access_token 41 | } else { 42 | // eslint-disable-next-line no-throw-literal 43 | throw `getAccessToken error ${JSON.stringify(await resp.text())}` 44 | } 45 | } 46 | 47 | /** 48 | * Get & store siteID for finding sharepoint resources 49 | * 50 | * @param {string} accessToken token for accessing graph API 51 | */ 52 | export async function getSiteID(accessToken) { 53 | let data = await BUCKET.get('sharepoint', 'json') 54 | if (!data) { 55 | const resp = await fetch(`${config.apiEndpoint.graph}${config.baseResource}?$select=id`, { 56 | headers: { 57 | Authorization: `bearer ${accessToken}` 58 | } 59 | }) 60 | if (resp.ok) { 61 | data = await resp.json() 62 | console.log('Got & stored site-id.') 63 | await BUCKET.put('sharepoint', JSON.stringify({ id: data.id })) 64 | } 65 | } 66 | return data.id 67 | } 68 | -------------------------------------------------------------------------------- /assets/chinese.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/files/load.js: -------------------------------------------------------------------------------- 1 | import config from '../config/default' 2 | import { getAccessToken } from '../auth/onedrive' 3 | 4 | /** 5 | * Cloudflare cache instance 6 | */ 7 | const cache = caches.default 8 | 9 | /** 10 | * Cache downloadUrl according to caching rules. 11 | * @param {Request} request client's request 12 | * @param {integer} fileSize 13 | * @param {string} downloadUrl 14 | * @param {function} fallback handle function if the rules is not satisfied 15 | */ 16 | async function setCache(request, fileSize, downloadUrl, fallback) { 17 | if (fileSize < config.cache.entireFileCacheLimit) { 18 | console.info(`Cache entire file ${request.url}`) 19 | const remoteResp = await fetch(downloadUrl) 20 | const resp = new Response(remoteResp.body, { 21 | headers: { 22 | 'Content-Type': remoteResp.headers.get('Content-Type'), 23 | ETag: remoteResp.headers.get('ETag') 24 | }, 25 | status: remoteResp.status, 26 | statusText: remoteResp.statusText 27 | }) 28 | await cache.put(request, resp.clone()) 29 | return resp 30 | } else if (fileSize < config.cache.chunkedCacheLimit) { 31 | console.info(`Chunk cache file ${request.url}`) 32 | const remoteResp = await fetch(downloadUrl) 33 | const { readable, writable } = new TransformStream() 34 | remoteResp.body.pipeTo(writable) 35 | const resp = new Response(readable, { 36 | headers: { 37 | 'Content-Type': remoteResp.headers.get('Content-Type'), 38 | ETag: remoteResp.headers.get('ETag') 39 | }, 40 | status: remoteResp.status, 41 | statusText: remoteResp.statusText 42 | }) 43 | await cache.put(request, resp.clone()) 44 | return resp 45 | } else { 46 | console.info(`No cache ${request.url} because file_size(${fileSize}) > limit(${config.cache.chunkedCacheLimit})`) 47 | return await fallback(downloadUrl) 48 | } 49 | } 50 | 51 | /** 52 | * Redirect to the download url. 53 | * @param {string} downloadUrl 54 | */ 55 | async function directDownload(downloadUrl) { 56 | console.info(`DirectDownload -> ${downloadUrl}`) 57 | return new Response(null, { 58 | status: 302, 59 | headers: { 60 | Location: downloadUrl.slice(6) 61 | } 62 | }) 63 | } 64 | 65 | /** 66 | * Download a file using Cloudflare as a relay. 67 | * @param {string} downloadUrl 68 | */ 69 | async function proxiedDownload(downloadUrl) { 70 | console.info(`ProxyDownload -> ${downloadUrl}`) 71 | const remoteResp = await fetch(downloadUrl) 72 | const { readable, writable } = new TransformStream() 73 | remoteResp.body.pipeTo(writable) 74 | return new Response(readable, remoteResp) 75 | } 76 | 77 | export async function handleFile(request, pathname, downloadUrl, { proxied = false, fileSize = 0 }) { 78 | if (config.cache && config.cache.enable && config.cache.paths.filter(p => pathname.startsWith(p)).length > 0) { 79 | return setCache(request, fileSize, downloadUrl, proxied ? proxiedDownload : directDownload) 80 | } 81 | return (proxied ? proxiedDownload : directDownload)(downloadUrl) 82 | } 83 | 84 | export async function handleUpload(request, pathname, filename) { 85 | const url = `${config.apiEndpoint.graph}/me/drive/root:${encodeURI(config.base) + 86 | (pathname.slice(-1) === '/' ? pathname : pathname + '/')}${filename}:/content` 87 | return await fetch(url, { 88 | method: 'PUT', 89 | headers: { 90 | Authorization: `bearer ${await getAccessToken()}`, 91 | ...request.headers 92 | }, 93 | body: request.body 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ag_dubs@cloudflare.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/config/default.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-irregular-whitespace */ 2 | const config = { 3 | /** 4 | * Configure the account/resource type for deployment (with 0 or 1) 5 | * - accountType: controls account type, 0 for global, 1 for china (21Vianet) 6 | * - driveType: controls drive resource type, 0 for onedrive, 1 for sharepoint document 7 | * 8 | * Followed keys is used for sharepoint resource, change them only if you gonna use sharepoint 9 | * - hostName: sharepoint site hostname (e.g. 'name.sharepoint.com') 10 | * - sitePath: sharepoint site path (e.g. '/sites/name') 11 | * !Note: we do not support deploying onedrive & sharepoint at the same time 12 | */ 13 | type: { 14 | accountType: 0, 15 | driveType: 0, 16 | hostName: null, 17 | sitePath: null 18 | }, 19 | 20 | refresh_token: REFRESH_TOKEN, 21 | client_id: '6600e358-9328-4050-af82-0af9cdde796b', 22 | client_secret: CLIENT_SECRET, 23 | 24 | /** 25 | * Exactly the same `redirect_uri` in your Azure Application 26 | */ 27 | redirect_uri: 'http://localhost', 28 | 29 | /** 30 | * The base path for indexing, all files and subfolders are public by this tool. For example: `/Public`. 31 | */ 32 | base: '/Public', 33 | 34 | /** 35 | * Feature: Pagination when a folder has multiple(>${top}) files 36 | * - top: specify the page size limit of the result set, a big `top` value will slow down the fetching speed 37 | */ 38 | pagination: { 39 | enable: true, 40 | top: 100 // default: 200, accepts a minimum value of 1 and a maximum value of 999 (inclusive) 41 | }, 42 | 43 | /** 44 | * Feature Caching 45 | * Enable Cloudflare cache for path pattern listed below. 46 | * Cache rules: 47 | * - Entire File Cache 0 < file_size < entireFileCacheLimit 48 | * - Chunked Cache entireFileCacheLimit <= file_size < chunkedCacheLimit 49 | * - No Cache ( redirect to OneDrive Server ) others 50 | * 51 | * Difference between `Entire File Cache` and `Chunked Cache` 52 | * 53 | * `Entire File Cache` requires the entire file to be transferred to the Cloudflare server before 54 | * the first byte sent to a client. 55 | * 56 | * `Chunked Cache` would stream the file content to the client while caching it. 57 | * But there is no exact Content-Length in the response headers. ( Content-Length: chunked ) 58 | * 59 | * `previewCache`: using CloudFlare cache to preview 60 | */ 61 | cache: { 62 | enable: true, 63 | entireFileCacheLimit: 10000000, // 10MB 64 | chunkedCacheLimit: 100000000, // 100MB 65 | previewCache: false, 66 | paths: ['/🥟%20Some%20test%20files/Previews'] 67 | }, 68 | 69 | /** 70 | * Feature: Thumbnail 71 | * Show a thumbnail of image by ?thumbnail=small (small, medium, large) 72 | * More details: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_thumbnails?view=odsp-graph-online#size-options 73 | * Example: https://storage.spencerwoo.com/🥟%20Some%20test%20files/Previews/eb37c02438f.png?thumbnail=mediumSquare 74 | * You can embed this link (url encoded) directly inside Markdown or HTML. 75 | */ 76 | thumbnail: true, 77 | 78 | /** 79 | * Small File Upload (<= 4MB) 80 | * POST https:////?upload=&key= 81 | * The is defined by you 82 | */ 83 | upload: { 84 | enable: false, 85 | key: 'your_secret_key_here' 86 | }, 87 | 88 | /** 89 | * Feature: Proxy Download 90 | * Use Cloudflare as a relay to speed up download. (Especially in Mainland China) 91 | * Example: https://storage.spencerwoo.com/🥟%20Some%20test%20files/Previews/eb37c02438f.png?raw&proxied 92 | * You can also embed this link (url encoded) directly inside Markdown or HTML. 93 | */ 94 | proxyDownload: true 95 | } 96 | 97 | // IIFE to set apiEndpoint & baseResource 98 | // eslint-disable-next-line no-unused-expressions 99 | !(function({ accountType, driveType, hostName, sitePath }) { 100 | config.apiEndpoint = { 101 | graph: accountType ? 'https://microsoftgraph.chinacloudapi.cn/v1.0' : 'https://graph.microsoft.com/v1.0', 102 | auth: accountType 103 | ? 'https://login.chinacloudapi.cn/common/oauth2/v2.0' 104 | : 'https://login.microsoftonline.com/common/oauth2/v2.0' 105 | } 106 | config.baseResource = driveType ? `/sites/${hostName}:${sitePath}` : '/me/drive' 107 | })(config.type) 108 | 109 | export default config 110 | -------------------------------------------------------------------------------- /assets/english.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/folderView.js: -------------------------------------------------------------------------------- 1 | import emojiRegex from 'emoji-regex/RGI_Emoji' 2 | import { getClassNameForMimeType, getClassNameForFilename } from 'font-awesome-filetypes' 3 | 4 | import { renderHTML } from './render/htmlWrapper' 5 | import { renderPath } from './render/pathUtil' 6 | import { renderMarkdown } from './render/mdRenderer' 7 | 8 | /** 9 | * Convert bytes to human readable file size 10 | * 11 | * @param {Number} bytes File size in bytes 12 | * @param {Boolean} si 1000 - true; 1024 - false 13 | */ 14 | function readableFileSize(bytes, si) { 15 | bytes = parseInt(bytes, 10) 16 | var thresh = si ? 1000 : 1024 17 | if (Math.abs(bytes) < thresh) { 18 | return bytes + ' B' 19 | } 20 | var units = si 21 | ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 22 | : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] 23 | var u = -1 24 | do { 25 | bytes /= thresh 26 | ++u 27 | } while (Math.abs(bytes) >= thresh && u < units.length - 1) 28 | return bytes.toFixed(1) + ' ' + units[u] 29 | } 30 | 31 | /** 32 | * Render Folder Index 33 | * 34 | * @param {*} items 35 | * @param {*} isIndex don't show ".." on index page. 36 | */ 37 | export async function renderFolderView(items, path, request) { 38 | const isIndex = path === '/' 39 | 40 | const el = (tag, attrs, content) => `<${tag} ${attrs.join(' ')}>${content}` 41 | const div = (className, content) => el('div', [`class=${className}`], content) 42 | const item = (icon, fileName, fileAbsoluteUrl, size, emojiIcon) => 43 | el( 44 | 'a', 45 | [`href="${fileAbsoluteUrl}"`, 'class="item"', size ? `size="${size}"` : ''], 46 | (emojiIcon ? el('i', ['style="font-style: normal"'], emojiIcon) : el('i', [`class="${icon}"`], '')) + 47 | fileName + 48 | el('div', ['style="flex-grow: 1;"'], '') + 49 | (fileName === '..' ? '' : el('span', ['class="size"'], readableFileSize(size))) 50 | ) 51 | 52 | const intro = `
53 |

Yoo, I'm Spencer Woo 👋

54 |

This is Spencer's OneDrive public directory listing. Feel free to download any files that you find useful. Reach me at: spencer.wushangbo [at] gmail [dot] com.

55 |

Portfolio · Blog · GitHub

56 |
` 57 | 58 | // Check if current directory contains README.md, if true, then render spinner 59 | let readmeExists = false 60 | let readmeFetchUrl = '' 61 | 62 | const body = div( 63 | 'container', 64 | div('path', renderPath(path)) + 65 | div( 66 | 'items', 67 | el( 68 | 'div', 69 | ['style="min-width: 600px"'], 70 | (!isIndex ? item('far fa-folder', '..', `${path}..`) : '') + 71 | items 72 | .map(i => { 73 | // Check if the current item is a folder or a file 74 | if ('folder' in i) { 75 | const emoji = emojiRegex().exec(i.name) 76 | if (emoji && !emoji.index) { 77 | return item('', i.name.replace(emoji, '').trim(), `${path}${i.name}/`, i.size, emoji[0]) 78 | } else { 79 | return item('far fa-folder', i.name, `${path}${i.name}/`, i.size) 80 | } 81 | } else if ('file' in i) { 82 | // Check if README.md exists 83 | if (!readmeExists) { 84 | // TODO: debugging for README preview rendering 85 | console.log(i) 86 | 87 | readmeExists = i.name.toLowerCase() === 'readme.md' 88 | readmeFetchUrl = i['@microsoft.graph.downloadUrl'] 89 | } 90 | 91 | // Render file icons 92 | let fileIcon = getClassNameForMimeType(i.file.mimeType) 93 | if (fileIcon === 'fa-file') { 94 | // Check for files that haven't been rendered as expected 95 | const extension = i.name.split('.').pop() 96 | if (extension === 'md') { 97 | fileIcon = 'fab fa-markdown' 98 | } else if (['7z', 'rar', 'bz2', 'xz', 'tar', 'wim'].includes(extension)) { 99 | fileIcon = 'far fa-file-archive' 100 | } else if (['flac', 'oga', 'opus'].includes(extension)) { 101 | fileIcon = 'far fa-file-audio' 102 | } else { 103 | fileIcon = `far ${getClassNameForFilename(i.name)}` 104 | } 105 | } else { 106 | fileIcon = `far ${fileIcon}` 107 | } 108 | return item(fileIcon, i.name, `${path}${i.name}`, i.size) 109 | } else { 110 | console.log(`unknown item type ${i}`) 111 | } 112 | }) 113 | .join('') 114 | ) 115 | ) + 116 | (readmeExists && !isIndex ? await renderMarkdown(readmeFetchUrl, 'fade-in-fwd', '') : '') + 117 | (isIndex ? intro : '') 118 | ) 119 | return renderHTML(body, ...[request.pLink, request.pIdx]) 120 | } 121 | -------------------------------------------------------------------------------- /src/render/htmlWrapper.js: -------------------------------------------------------------------------------- 1 | import { favicon } from './favicon' 2 | 3 | const COMMIT_HASH = 'ad7b598' 4 | 5 | const pagination = (pIdx, attrs) => { 6 | const getAttrs = (c, h, isNext) => 7 | `class="${c}" ${h ? `href="pagination?page=${h}"` : ''} ${isNext === undefined ? '' : `id=${c.includes('pre') ? 'pagination-pre' : 'pagination-next'}` 8 | }` 9 | if (pIdx) { 10 | switch (pIdx) { 11 | case pIdx < 0 ? pIdx : null: 12 | attrs = [getAttrs('pre', -pIdx - 1, 0), getAttrs('next off', null)] 13 | break 14 | case 1: 15 | attrs = [getAttrs('pre off', null), getAttrs('next', pIdx + 1, 1)] 16 | break 17 | default: 18 | attrs = [getAttrs('pre', pIdx - 1, 0), getAttrs('next', pIdx + 1, 1)] 19 | } 20 | return `${` PREV`}Page ${pIdx} ${`NEXT `}` 21 | } 22 | return '' 23 | } 24 | 25 | export function renderHTML(body, pLink, pIdx) { 26 | pLink = pLink || '' 27 | const p = 'window[pLinkId]' 28 | 29 | return ` 30 | 31 | 32 | 33 | 34 | 35 | Spencer's OneDrive 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ${body} 54 |
${pagination(pIdx)}
55 |
56 | 57 | 105 | 106 | ` 107 | } 108 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import config from './config/default' 2 | import { AUTH_ENABLED, NAME, ENABLE_PATHS } from './auth/config' 3 | import { parseAuthHeader, unauthorizedResponse } from './auth/credentials' 4 | import { getAccessToken, getSiteID } from './auth/onedrive' 5 | import { handleFile, handleUpload } from './files/load' 6 | import { extensions } from './render/fileExtension' 7 | import { renderFolderView } from './folderView' 8 | import { renderFilePreview } from './fileView' 9 | 10 | addEventListener('fetch', event => { 11 | event.respondWith(handle(event.request)) 12 | }) 13 | 14 | async function handle(request) { 15 | if (AUTH_ENABLED === false) { 16 | return handleRequest(request) 17 | } 18 | 19 | if (AUTH_ENABLED === true) { 20 | const pathname = decodeURIComponent(new URL(request.url).pathname).toLowerCase() 21 | const privatePaths = ENABLE_PATHS.map(i => i.toLowerCase()) 22 | 23 | if (privatePaths.filter(p => pathname.toLowerCase().startsWith(p)).length > 0 || /__Lock__/gi.test(pathname)) { 24 | const credentials = parseAuthHeader(request.headers.get('Authorization')) 25 | 26 | if (!credentials || credentials.name !== NAME || credentials.pass !== AUTH_PASSWORD) { 27 | return unauthorizedResponse('Unauthorized') 28 | } 29 | 30 | return handleRequest(request) 31 | } else { 32 | return handleRequest(request) 33 | } 34 | } else { 35 | console.info('Auth error unexpected.') 36 | } 37 | } 38 | 39 | // Cloudflare cache instance 40 | const cache = caches.default 41 | const base = encodeURI(config.base).replace(/\/$/, '') 42 | 43 | /** 44 | * Format and regularize directory path for OneDrive API 45 | * 46 | * @param {string} pathname The absolute path to file 47 | * @param {boolean} isRequestFolder is indexing folder or not 48 | */ 49 | function wrapPathName(pathname, isRequestFolder) { 50 | pathname = base + pathname 51 | const isIndexingRoot = pathname === '/' 52 | if (isRequestFolder) { 53 | if (isIndexingRoot) return '' 54 | return `:${pathname.replace(/\/$/, '')}:` 55 | } 56 | return `:${pathname}` 57 | } 58 | 59 | async function handleRequest(request) { 60 | if (config.cache && config.cache.enable) { 61 | const maybeResponse = await cache.match(request) 62 | if (maybeResponse) return maybeResponse 63 | } 64 | 65 | const accessToken = await getAccessToken() 66 | if (config.type.driveType) { 67 | config.baseResource = `/sites/${await getSiteID(accessToken)}/drive` 68 | } 69 | 70 | const { pathname, searchParams } = new URL(request.url) 71 | const neoPathname = pathname.replace(/pagination$/, '') 72 | const isRequestFolder = pathname.endsWith('/') || searchParams.get('page') 73 | 74 | const rawFile = searchParams.get('raw') !== null 75 | const thumbnail = config.thumbnail ? searchParams.get('thumbnail') : false 76 | const proxied = config.proxyDownload ? searchParams.get('proxied') !== null : false 77 | 78 | if (thumbnail) { 79 | const url = `${config.apiEndpoint.graph}${config.baseResource}/root${wrapPathName( 80 | neoPathname, 81 | isRequestFolder 82 | )}:/thumbnails/0/${thumbnail}/content` 83 | const resp = await fetch(url, { 84 | headers: { 85 | Authorization: `bearer ${accessToken}` 86 | } 87 | }) 88 | 89 | return await handleFile(request, pathname, resp.url, { 90 | proxied 91 | }) 92 | } 93 | 94 | let url = `${config.apiEndpoint.graph}${config.baseResource}/root${wrapPathName(neoPathname, isRequestFolder)}${ 95 | isRequestFolder 96 | ? '/children' + (config.pagination.enable && config.pagination.top ? `?$top=${config.pagination.top}` : '') 97 | : '?select=%40microsoft.graph.downloadUrl,name,size,file' 98 | }` 99 | 100 | // get & set {pLink ,pIdx} for fetching and paging 101 | const paginationLink = request.headers.get('pLink') 102 | const paginationIdx = request.headers.get('pIdx') - 0 103 | 104 | if (paginationLink && paginationLink !== 'undefined') { 105 | url += `&$skiptoken=${paginationLink}` 106 | } 107 | 108 | const resp = await fetch(url, { 109 | headers: { 110 | Authorization: `bearer ${accessToken}` 111 | } 112 | }) 113 | 114 | let error = null 115 | if (resp.ok) { 116 | const data = await resp.json() 117 | if (data['@odata.nextLink']) { 118 | request.pIdx = paginationIdx || 1 119 | request.pLink = data['@odata.nextLink'].match(/&\$skiptoken=(.+)/)[1] 120 | } else if (paginationIdx) { 121 | request.pIdx = -paginationIdx 122 | } 123 | if ('file' in data) { 124 | // Render file preview view or download file directly 125 | const fileExt = data.name 126 | .split('.') 127 | .pop() 128 | .toLowerCase() 129 | 130 | // Render file directly if url params 'raw' are given 131 | if (rawFile || !(fileExt in extensions)) { 132 | return await handleFile(request, pathname, data['@microsoft.graph.downloadUrl'], { 133 | proxied, 134 | fileSize: data.size 135 | }) 136 | } 137 | 138 | // Add preview by CloudFlare worker cache feature 139 | let cacheUrl = null 140 | if (config.cache.enable && config.cache.previewCache && data.size < config.cache.chunkedCacheLimit) { 141 | cacheUrl = request.url + '?proxied&raw' 142 | } 143 | 144 | return new Response(await renderFilePreview(data, pathname, fileExt, cacheUrl || null), { 145 | headers: { 146 | 'Access-Control-Allow-Origin': '*', 147 | 'content-type': 'text/html' 148 | } 149 | }) 150 | } else { 151 | // Render folder view, list all children files 152 | if (config.upload && request.method === 'POST') { 153 | const filename = searchParams.get('upload') 154 | const key = searchParams.get('key') 155 | if (filename && key && config.upload.key === key) { 156 | return await handleUpload(request, neoPathname, filename) 157 | } else { 158 | return new Response('', { 159 | status: 400 160 | }) 161 | } 162 | } 163 | 164 | // 302 all folder requests that doesn't end with / 165 | if (!isRequestFolder) { 166 | return Response.redirect(request.url + '/', 302) 167 | } 168 | 169 | return new Response(await renderFolderView(data.value, neoPathname, request), { 170 | headers: { 171 | 'Access-Control-Allow-Origin': '*', 172 | 'content-type': 'text/html' 173 | } 174 | }) 175 | } 176 | } else { 177 | error = (await resp.json()).error 178 | } 179 | 180 | if (error) { 181 | const body = JSON.stringify(error) 182 | switch (error.code) { 183 | case 'itemNotFound': 184 | return new Response(body, { 185 | status: 404, 186 | headers: { 187 | 'content-type': 'application/json' 188 | } 189 | }) 190 | default: 191 | return new Response(body, { 192 | status: 500, 193 | headers: { 194 | 'content-type': 'application/json' 195 | } 196 | }) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /themes/spencer.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.cnpmjs.org/css2?family=Inter:wght@400;700&display=swap'); 2 | 3 | :root { 4 | --base-font-family: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, pingfang sc, 5 | source han sans sc, noto sans cjk sc, sarasa gothic sc, microsoft yahei, sans-serif, Apple Color Emoji, 6 | Segoe UI Emoji; 7 | } 8 | 9 | html, 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | body { 16 | font-family: var(--base-font-family); 17 | -webkit-font-smoothing: antialiased; 18 | background: #fafafa; 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-between; 22 | flex-flow: column nowrap; 23 | height: 100vh; 24 | } 25 | 26 | nav { 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | width: 100%; 31 | min-height: 2rem; 32 | padding: 0.8rem 0rem; 33 | background: #ffffff; 34 | color: #000000; 35 | font-size: 1.4rem; 36 | box-shadow: inset 0 -1px #eaeaea; 37 | margin-bottom: 1.5rem; 38 | } 39 | 40 | a { 41 | transition: 0.2s all ease-in-out; 42 | color: #0070f3; 43 | } 44 | 45 | .brand { 46 | font-weight: bold; 47 | } 48 | 49 | .container, 50 | .paginate-container { 51 | width: calc(100% - 40px); 52 | max-width: 800px; 53 | margin: 0 auto; 54 | } 55 | 56 | .path { 57 | margin-bottom: 1.5rem; 58 | font-size: 0.88rem; 59 | color: #999; 60 | } 61 | 62 | .path a { 63 | text-decoration: none; 64 | color: #333; 65 | } 66 | 67 | .items { 68 | overflow-x: auto; 69 | display: flex; 70 | flex-flow: column nowrap; 71 | background: white; 72 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12); 73 | border-radius: 8px; 74 | } 75 | 76 | a.item { 77 | display: flex; 78 | align-items: center; 79 | text-decoration: none; 80 | color: #000000; 81 | padding: 0.8rem 1rem; 82 | transition: 0.2s all ease-in-out; 83 | } 84 | 85 | a.item i { 86 | margin-right: 0.5rem; 87 | } 88 | 89 | a.item .size { 90 | opacity: 0.6; 91 | } 92 | 93 | footer { 94 | font-size: 0.8rem; 95 | color: #666; 96 | width: calc(100% - 40px); 97 | padding: 1.6rem 0rem; 98 | text-align: center; 99 | } 100 | 101 | footer a { 102 | text-decoration: none; 103 | } 104 | 105 | a:hover, 106 | a.item:hover { 107 | opacity: 0.6; 108 | } 109 | 110 | .download-button-container { 111 | width: 100%; 112 | text-align: center; 113 | box-sizing: border-box; 114 | } 115 | 116 | .download-button { 117 | display: block; 118 | background-color: #000; 119 | color: #ffffff; 120 | cursor: pointer; 121 | font-weight: bold; 122 | text-decoration: none; 123 | padding: 0.5rem 1rem; 124 | margin: 1.5rem auto; 125 | max-width: 180px; 126 | user-select: none; 127 | border-radius: 2px; 128 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12); 129 | } 130 | 131 | .markdown-body { 132 | font-family: var(--base-font-family) !important; 133 | margin-top: 1.5rem; 134 | } 135 | 136 | .loading-label { 137 | font-family: var(--base-font-family) !important; 138 | text-align: center; 139 | margin: 3rem 0rem; 140 | opacity: 0.6; 141 | } 142 | 143 | .paginate-container { 144 | margin-top: 1.5rem; 145 | display: grid; 146 | justify-content: space-between; 147 | grid-template-columns: repeat(3, auto); 148 | } 149 | 150 | .paginate-container a { 151 | cursor: pointer; 152 | text-decoration: none; 153 | } 154 | 155 | .paginate-container a.off { 156 | pointer-events: none; 157 | opacity: 0.5; 158 | } 159 | 160 | .fade-in-bottom { 161 | -webkit-animation: fade-in-bottom 0.3s cubic-bezier(0.39, 0.575, 0.565, 1) both; 162 | animation: fade-in-bottom 0.3s cubic-bezier(0.39, 0.575, 0.565, 1) both; 163 | } 164 | 165 | .fade-in-fwd { 166 | -webkit-animation: fade-in-fwd 0.6s cubic-bezier(0.39, 0.575, 0.565, 1) both; 167 | animation: fade-in-fwd 0.6s cubic-bezier(0.39, 0.575, 0.565, 1) both; 168 | } 169 | 170 | .fade-out-bck { 171 | -webkit-animation: fade-out-bck 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; 172 | animation: fade-out-bck 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; 173 | } 174 | 175 | @-webkit-keyframes fade-in-bottom { 176 | 0% { 177 | -webkit-transform: translateY(50px); 178 | transform: translateY(50px); 179 | opacity: 0; 180 | } 181 | 100% { 182 | -webkit-transform: translateY(0); 183 | transform: translateY(0); 184 | opacity: 1; 185 | } 186 | } 187 | 188 | @keyframes fade-in-bottom { 189 | 0% { 190 | -webkit-transform: translateY(50px); 191 | transform: translateY(50px); 192 | opacity: 0; 193 | } 194 | 100% { 195 | -webkit-transform: translateY(0); 196 | transform: translateY(0); 197 | opacity: 1; 198 | } 199 | } 200 | 201 | @-webkit-keyframes fade-in-fwd { 202 | 0% { 203 | -webkit-transform: translateZ(-80px); 204 | transform: translateZ(-80px); 205 | opacity: 0; 206 | } 207 | 100% { 208 | -webkit-transform: translateZ(0); 209 | transform: translateZ(0); 210 | opacity: 1; 211 | } 212 | } 213 | 214 | @keyframes fade-in-fwd { 215 | 0% { 216 | -webkit-transform: translateZ(-80px); 217 | transform: translateZ(-80px); 218 | opacity: 0; 219 | } 220 | 100% { 221 | -webkit-transform: translateZ(0); 222 | transform: translateZ(0); 223 | opacity: 1; 224 | } 225 | } 226 | 227 | @-webkit-keyframes fade-out-bck { 228 | 0% { 229 | -webkit-transform: translateZ(0); 230 | transform: translateZ(0); 231 | opacity: 1; 232 | } 233 | 100% { 234 | -webkit-transform: translateZ(-80px); 235 | transform: translateZ(-80px); 236 | opacity: 0; 237 | } 238 | } 239 | 240 | @keyframes fade-out-bck { 241 | 0% { 242 | -webkit-transform: translateZ(0); 243 | transform: translateZ(0); 244 | opacity: 1; 245 | } 246 | 100% { 247 | -webkit-transform: translateZ(-80px); 248 | transform: translateZ(-80px); 249 | opacity: 0; 250 | } 251 | } 252 | 253 | @media (prefers-color-scheme: dark) { 254 | a.item, 255 | nav, 256 | .aplayer .aplayer-info .aplayer-music .aplayer-title, 257 | .download-button, 258 | .loading-label, 259 | .markdown-body, 260 | .markdown-body code, 261 | .markdown-body pre, 262 | .markdown-body .highlight pre, 263 | .markdown-body table td, 264 | .markdown-body table th, 265 | .paginate-container span, 266 | .path a { 267 | color: #a2a2a2 !important; 268 | } 269 | body, 270 | div.fade-in-fwd, 271 | div.intro, 272 | div.medium-zoom-overlay, 273 | embed.pdfobject, 274 | img.medium-zoom-image { 275 | background: #1b1b1b !important; 276 | } 277 | embed.pdfobject, 278 | img.medium-zoom-image { 279 | opacity: 0.75 !important; 280 | } 281 | nav, 282 | .aplayer .aplayer-info, 283 | .items, 284 | .markdown-body, 285 | .markdown-body img { 286 | background: #131313 !important; 287 | } 288 | .aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon:hover path { 289 | fill: #a2a2a2 !important; 290 | } 291 | .aplayer .aplayer-info .aplayer-music .aplayer-author, 292 | .path { 293 | color: #626262 !important; 294 | } 295 | .markdown-body code, 296 | .markdown-body pre, 297 | .markdown-body .highlight pre, 298 | .markdown-body table td, 299 | .markdown-body table th { 300 | background: #2b2b2b !important; 301 | } 302 | .token.atrule, 303 | .token.attr-value, 304 | .token.punctuation, 305 | .token.string { 306 | color: #afd5fb !important; 307 | } 308 | .language-css .token.string, 309 | .token.entity, 310 | .token.operator, 311 | .token.url { 312 | color: #89befa !important; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/fileView.js: -------------------------------------------------------------------------------- 1 | import marked from 'marked' 2 | 3 | import { renderHTML } from './render/htmlWrapper' 4 | import { renderPath } from './render/pathUtil' 5 | import { renderMarkdown } from './render/mdRenderer' 6 | 7 | import { preview, extensions } from './render/fileExtension' 8 | 9 | /** 10 | * Render code blocks with the help of marked and Markdown grammar 11 | * 12 | * @param {Object} file Object representing the code file to preview 13 | * @param {string} lang The markdown code language string, usually just the file extension 14 | */ 15 | async function renderCodePreview(file, lang) { 16 | const resp = await fetch(file['@microsoft.graph.downloadUrl']) 17 | const content = await resp.text() 18 | const toMarkdown = `\`\`\`${lang}\n${content}\n\`\`\`` 19 | const renderedCode = marked(toMarkdown) 20 | return `
21 | ${renderedCode} 22 |
` 23 | } 24 | 25 | /** 26 | * Render PDF with built-in PDF viewer 27 | * 28 | * @param {Object} file Object representing the PDF to preview 29 | */ 30 | function renderPDFPreview(file) { 31 | return `
32 |
33 | 34 | Loading PDF... 35 |
36 | ` 106 | } 107 | 108 | /** 109 | * Render image (jpg, png or gif) 110 | * 111 | * @param {Object} file Object representing the image to preview 112 | */ 113 | function renderImage(file) { 114 | return `
115 | ${file.name} 116 |
` 117 | } 118 | 119 | /** 120 | * Render video (mp4, flv, m3u8, webm ...) 121 | * 122 | * @param {Object} file Object representing the video to preview 123 | * @param {string} fileExt The file extension parsed 124 | */ 125 | function renderVideoPlayer(file, fileExt) { 126 | return `
127 | ` 137 | } 138 | 139 | /** 140 | * Render audio (mp3, aac, wav, oga ...) 141 | * 142 | * @param {Object} file Object representing the audio to preview 143 | */ 144 | function renderAudioPlayer(file) { 145 | return `
146 | ` 156 | } 157 | 158 | /** 159 | * File preview fallback 160 | * 161 | * @param {string} fileExt The file extension parsed 162 | */ 163 | function renderUnsupportedView(fileExt) { 164 | return `
165 |

Sorry, we don't support previewing .${fileExt} files as of today. You can download the file directly.

166 |
` 167 | } 168 | 169 | /** 170 | * Render preview of supported file format 171 | * 172 | * @param {Object} file Object representing the file to preview 173 | * @param {string} fileExt The file extension parsed 174 | */ 175 | async function renderPreview(file, fileExt, cacheUrl) { 176 | if (cacheUrl) { 177 | // This will change your download url too! (proxied download) 178 | file['@microsoft.graph.downloadUrl'] = cacheUrl 179 | } 180 | 181 | switch (extensions[fileExt]) { 182 | case preview.markdown: 183 | return await renderMarkdown(file['@microsoft.graph.downloadUrl'], '', 'style="margin-top: 0;"') 184 | 185 | case preview.text: 186 | return await renderCodePreview(file, '') 187 | 188 | case preview.image: 189 | return renderImage(file) 190 | 191 | case preview.code: 192 | return await renderCodePreview(file, fileExt) 193 | 194 | case preview.pdf: 195 | return renderPDFPreview(file) 196 | 197 | case preview.video: 198 | return renderVideoPlayer(file, fileExt) 199 | 200 | case preview.audio: 201 | return renderAudioPlayer(file) 202 | 203 | default: 204 | return renderUnsupportedView(fileExt) 205 | } 206 | } 207 | 208 | export async function renderFilePreview(file, path, fileExt, cacheUrl) { 209 | const el = (tag, attrs, content) => `<${tag} ${attrs.join(' ')}>${content}` 210 | const div = (className, content) => el('div', [`class=${className}`], content) 211 | 212 | const body = div( 213 | 'container', 214 | div('path', renderPath(path) + ` / ${file.name}`) + 215 | div('items', el('div', ['style="padding: 1rem 1rem;"'], await renderPreview(file, fileExt, cacheUrl))) + 216 | div( 217 | 'download-button-container', 218 | el( 219 | 'a', 220 | ['class="download-button"', `href="${file['@microsoft.graph.downloadUrl']}"`, 'data-turbolinks="false"'], 221 | ' DOWNLOAD' 222 | ) 223 | ) 224 | ) 225 | return renderHTML(body) 226 | } 227 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 |
2 | onedrive-cf-index 3 |

onedrive-cf-index

4 | 由 CloudFlare Workers 强力驱动的 OneDrive 索引 5 |
6 | 7 | --- 8 | 9 | [![Hosted on Cloudflare Workers](https://img.shields.io/badge/Hosted%20on-CF%20Workers-f38020?logo=cloudflare&logoColor=f38020&labelColor=282d33)](https://storage.spencerwoo.com/) 10 | [![Deploy](https://github.com/spencerwooo/onedrive-cf-index/workflows/Deploy/badge.svg)](https://github.com/spencerwooo/onedrive-cf-index/actions?query=workflow%3ADeploy) 11 | [![README-CN](assets/chinese.svg)](./README-CN.md) 12 | 13 |
本项目使用 CloudFlare Workers 帮助你免费部署与分享你的 OneDrive 文件。本项目极大源自:onedrive-index-cloudflare-worker,致敬。
14 | 15 | ## Demo 16 | 17 | 在线演示:[Spencer's OneDrive Index](https://storage.spencerwoo.com/). 18 | 19 | ![Screenshot Demo](assets/screenshot.png) 20 | 21 | ## 功能 22 | 23 | ### 🚀 功能一览 24 | 25 | - 全新「面包屑」导航栏; 26 | - 令牌凭证由 Cloudflare Workers 自动刷新,并保存于(免费的)全局 KV 存储中; 27 | - 使用 [Turbolinks®](https://github.com/turbolinks/turbolinks) 实现路由懒加载; 28 | - 支持由世纪互联运营的 OneDrive 版本; 29 | - 支持 SharePoint 部署; 30 | 31 | ### 🗃️ 目录索引显示 32 | 33 | - 全新支持自定义的设计风格:[spencer.css](themes/spencer.css); 34 | - 支持使用 Emoji 作为文件夹图标(如果文件夹名称第一位是 Emoji 则自动开启该功能); 35 | - 渲染 `README.md` 如果当前目录下包含此文件,使用 [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) 渲染样式; 36 | - 支持「分页」,没有一个目录仅限显示 200 个项目的限制了! 37 | 38 | ### 📁 文件在线预览 39 | 40 | - 根据文件类型渲染文件图标,图标使用 [Font Awesome icons](https://fontawesome.com/); 41 | - 支持预览: 42 | - 纯文本:`.txt`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/iso_8859-1.txt). 43 | - Markdown 格式文本:`.md`, `.mdown`, `.markdown`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/i_m_a_md.md). 44 | - 图片(支持 Medium 风格的图片缩放):`.png`, `.jpg`, and `.gif`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/). 45 | - 代码高亮:`.js`, `.py`, `.c`, `.json`... [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Code/pathUtil.js). 46 | - PDF(支持懒加载、加载进度、Chrome 内置 PDF 阅读器):`.pdf`. [_DEMO_](). 47 | - 音乐:`.mp3`, `.aac`, `.wav`, `.oga`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Multimedia/Elysian%20Fields%20-%20Climbing%20My%20Dark%20Hair.mp3). 48 | - 视频:`.mp4`, `.flv`, `.webm`, `.m3u8`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Multimedia/%E8%BD%A6%E5%BA%93%E5%A5%B3%E7%8E%8B%20%E9%AB%98%E8%B7%9F%E8%B9%A6%E8%BF%AA%20%E4%B9%98%E9%A3%8E%E7%A0%B4%E6%B5%AA%E7%9A%84%E5%A7%90%E5%A7%90%E4%B8%BB%E9%A2%98%E6%9B%B2%E3%80%90%E9%86%8B%E9%86%8B%E3%80%91.mp4). 49 | 50 | ### 🔒 私有文件夹 51 | 52 | ![Private folders](assets/private-folder.png) 53 | 54 | 我们可以给某个特定的文件夹(目录)上锁,需要认证才能访问。我们可以在 `src/auth/config.js` 文件中将我们想要设为私有文件夹的目录写入 `ENABLE_PATHS` 列表中。我们还可以自定义认证所使用的用户名 `NAME` 以及密码,其中认证密码保存于 `AUTH_PASSWORD` 环境变量中,需要使用 wrangler 来设置这一环境变量: 55 | 56 | ```bash 57 | wrangler secret put AUTH_PASSWORD 58 | # 在这里输入你自己的认证密码 59 | ``` 60 | 61 | 有关 wrangler 的使用细节等详细内容,请参考 [接下来的部分段落](#准备工作)。 62 | 63 | ### ⬇️ 代理下载文件 / 文件直链访问 64 | 65 | - [可选] Proxied download(代理下载文件):`?proxied` - 经由 CloudFlare Workers 下载文件,如果(1)`config/default.js` 中的 `proxyDownload` 为 `true`,以及(2)使用参数 `?proxied` 请求文件; 66 | - [可选] Raw file download(文件直链访问):`?raw` - 返回文件直链而不是预览界面; 67 | - 两个参数可以一起使用,即 `?proxied&raw` 和 `?raw&proxied` 均有效。 68 | 69 | 是的,这也就意味着你可以将这一项目用来搭建「图床」,或者用于搭建静态文件部署服务,比如下面的图片链接: 70 | 71 | ``` 72 | https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/nyancat.gif?raw 73 | ``` 74 | 75 | ![](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/nyancat.gif?raw) 76 | 77 | ### 其他功能 78 | 79 | 请参考原项目的「🔥 新特性 V1.1」部分:[onedrive-index-cloudflare-worker](https://github.com/heymind/OneDrive-Index-Cloudflare-Worker#-%E6%96%B0%E7%89%B9%E6%80%A7-v11),**但我不保证全部功能均可用,因为本项目改动部分很大。** 80 | 81 | ## 部署指南 82 | 83 | _又臭又长的中文版部署指南预警!_ 84 | 85 | ### 生成 OneDrive API 令牌 86 | 87 | 1. 访问此 URL 创建新的 Blade app:[Microsoft Azure App registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)(普通版 OneDrive)或 [Microsoft Azure.cn App registrations](https://portal.azure.cn/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)(OneDrive 世纪互联版本),**建议将语言设置为「英语」以保证以下步骤中提到的模块和按钮的名称一致**: 88 | 89 | 1. 使用你的 Microsoft 账户登录,选择 `New registration`; 90 | 2. 在 `Name` 处设置 Blade app 的名称,比如 `my-onedrive-cf-index`; 91 | 3. 将 `Supported account types` 设置为 `Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)`。OneDrive 世纪互联用户设置为:`任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户`; 92 | 4. 将 `Redirect URI (optional)` 设置为 `Web`(下拉选项框)以及 `http://localhost`(URL 地址); 93 | 5. 点击 `Register`. 94 | 95 | ![](assets/register-app.png) 96 | 97 | 2. 在 `Overview` 面板获取你的 Application (client) ID - `client_id`: 98 | 99 | ![](assets/client-id.png) 100 | 101 | 3. 打开 `Certificates & secrets` 面板,点击 `New client secret`,创建一个新的叫做 `client_secret` 的 Client secret,并将 `Expires` 设置为 `Never`。点击 `Add` 并复制 `client_secret` 的 `Value` 并保存下来 **(仅有此一次机会)**: 102 | 103 | ![](assets/add-client-secret.png) 104 | 105 | 4. 打开 `API permissions` 面板,选择 `Microsoft Graph`,选择 `Delegated permissions`,并搜索 `offline_access, Files.Read, Files.Read.All` 这三个权限,**选择这三个权限,并点击 `Add permissions`:** 106 | 107 | ![](assets/add-permissions.png) 108 | 109 | 你应该成功开启这三个权限: 110 | 111 | ![](assets/permissions-used.png) 112 | 113 | 5. 获取 `refresh_token`,在本机(需要 Node.js 和 npm 环境,安装和推荐配置请参考 [准备工作](#准备工作))上面执行如下命令: 114 | 115 | ```sh 116 | npx @beetcb/ms-graph-cli 117 | ``` 118 | 119 |
demo gif
120 | 121 | 根据你自己的情况选择合适的选项,并输入我们上面获取到的一系列 token 令牌配置等,其中 `redirect_url` 可以直接设置为 `http://localhost`。有关命令行工具的具体使用方法请参考:[beetcb/ms-graph-cli](https://github.com/beetcb/ms-graph-cli)。 122 | 123 | 6. 最后,在我们的 OneDrive 中创建一个公共分享文件夹,比如 `/Public` 即可。建议不要直接分享根目录! 124 | 125 | 最后,这么折腾完,我们应该成功拿到如下的几个凭证: 126 | 127 | - `refresh_token` 128 | - `client_id` 129 | - `client_secret` 130 | - `redirect_uri` 131 | - `base`:默认为 `/Public`。 132 | 133 | _是,我知道很麻烦,但是这是微软,大家理解一下。🤷🏼‍♂️_ 134 | 135 | ### 准备工作 136 | 137 | Fork 再 clone 或者直接 clone 本仓库,并安装依赖 Node.js、`npm` 以及 `wrangler`。 138 | 139 | _强烈建议大家使用 Node version manager 比如 [n](https://github.com/tj/n) 或者 [nvm](https://github.com/nvm-sh/nvm) 安装 Node.js 和 `npm`,这样我们全局安装的 `wrangler` 就可以在我们的用户目录下安装保存配置文件了,也就不会遇到奇奇怪怪的权限问题了。_ 140 | 141 | ```sh 142 | # 安装 CloudFlare Workers 官方编译部署工具 143 | npm i @cloudflare/wrangler -g 144 | 145 | # 使用 npm 安装依赖 146 | npm install 147 | 148 | # 使用 wrangler 登录 CloudFlare 账户 149 | wrangler login 150 | 151 | # 使用这一命令检查自己的登录状态 152 | wrangler whoami 153 | ``` 154 | 155 | 打开 登录 CloudFlare,选择自己的域名,**再向下滚动一点,我们就能看到右侧栏处我们的 `account_id` 以及 `zone_id` 了。** 同时,在 `Workers` -> `Manage Workers` -> `Create a Worker` 处创建一个 **DRAFT** worker。 156 | 157 | 修改我们的 [`wrangler.toml`](wrangler.toml): 158 | 159 | - `name`:就是我们刚刚创建的 draft worker 名称,我们的 Worker 默认会发布到这一域名下:`..workers.dev`; 160 | - `account_id`:我们的 Cloudflare Account ID; 161 | - `zone_id`:我们的 Cloudflare Zone ID。 162 | 163 | 创建叫做 `BUCKET` 的 Cloudflare Workers KV bucket: 164 | 165 | ```sh 166 | # 创建 KV bucket 167 | wrangler kv:namespace create "BUCKET" 168 | 169 | # ... 或者,创建包括预览功能的 KV bucket 170 | wrangler kv:namespace create "BUCKET" --preview 171 | ``` 172 | 173 | 预览功能仅用于本地测试,和页面上的图片预览功能无关。 174 | 175 | 修改 [`wrangler.toml`](wrangler.toml) 里面的 `kv_namespaces`: 176 | 177 | - `kv_namespaces`:我们的 Cloudflare KV namespace,仅需替换 `id` 和(或者)`preview_id` 即可。_如果不需要预览功能,那么移除 `preview_id` 即可。_ 178 | 179 | 修改 [`src/config/default.js`](src/config/default.js): 180 | 181 | - `client_id`:刚刚获取的 OneDrive `client_id`; 182 | - `base`:之前创建的 `base` 目录; 183 | - 如果你部署常规国际版 OneDrive,那么忽略以下步骤即可; 184 | - 如果你部署的是由世纪互联运营的中国版 OneDrive: 185 | - 修改 `type` 下的 `accountType` 为 `1`; 186 | - 保持 `driveType` 不变; 187 | - 如果你部署的是 SharePoint 服务: 188 | - 保持 `accountType` 不变; 189 | - 修改 `driveType` 下的 `type` 为 `1`; 190 | - 并根据你的 SharePoint 服务修改 `hostName` 和 `sitePath`。 191 | 192 | 使用 `wrangler` 添加 Cloudflare Workers 环境变量(有关认证密码的介绍请见 [🔒 私有文件夹](#-私有文件夹)): 193 | 194 | ```sh 195 | # 添加我们的 refresh_token 和 client_secret 196 | wrangler secret put REFRESH_TOKEN 197 | # ... 并在这里粘贴我们的 refresh_token 198 | 199 | wrangler secret put CLIENT_SECRET 200 | # ... 并在这里粘贴我们的 client_secret 201 | 202 | wrangler secret put AUTH_PASSWORD 203 | # ... 在这里输入我们自己设置的认证密码 204 | ``` 205 | 206 | ### 编译与部署 207 | 208 | 我们可以使用 `wrangler` 预览部署: 209 | 210 | ```sh 211 | wrangler preview 212 | ``` 213 | 214 | 如果一切顺利,我们即可使用如下命令发布 Cloudflare Worker: 215 | 216 | ```sh 217 | wrangler publish 218 | ``` 219 | 220 | 我们也可以创建一个 GitHub Actions 来在每次 `push` 到 GitHub 仓库时自动发布新的 Worker,详情参考:[main.yml](.github/workflows/main.yml)。 221 | 222 | 如果想在自己的域名下部署 Cloudflare Worker,请参考:[How to Setup Cloudflare Workers on a Custom Domain](https://www.andressevilla.com/how-to-setup-cloudflare-workers-on-a-custom-domain/)。 223 | 224 | ## 样式、内容的自定义 225 | 226 | - 我们 **应该** 更改默认「着落页面」,直接修改 [src/folderView.js](src/folderView.js#L51-L55) 中 `intro` 的 HTML 即可; 227 | - 我们也 **应该** 更改页面的 header,直接修改 [src/render/htmlWrapper.js](src/render/htmlWrapper.js#L24) 即可; 228 | - 样式 CSS 文件位于 [themes/spencer.css](themes/spencer.css),可以根据自己需要自定义此文件,同时也需要更新 [src/render/htmlWrapper.js](src/render/htmlWrapper.js#L3) 文件中的 commit HASH; 229 | - 我们还可以自定义 Markdown 渲染 CSS 样式、PrismJS 代码高亮样式,等等等。 230 | 231 | --- 232 | 233 | 🏵 **onedrive-cf-index** ©Spencer Woo. Released under the MIT License. 234 | 235 | Authored and maintained by Spencer Woo. 236 | 237 | [@Portfolio](https://spencerwoo.com/) · [@Blog](https://blog.spencerwoo.com/) · [@GitHub](https://github.com/spencerwooo) 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | onedrive-cf-index 3 |

onedrive-cf-index

4 | Yet another OneDrive index, powered by CloudFlare Workers. 5 |
6 | 7 | ## Deprecated 8 | 9 | This project is deprecated and will not receive any further updates. Please use the newly refactored - [onedrive-vercel-index](https://github.com/spencerwooo/onedrive-vercel-index). 10 | 11 | - Streamlined deployment, no more setting up your custom client id or client secret anymore. 12 | - Better user experience, cleaner UI, more customisations. 13 | - Direct raw-file serving, proxied file serving. 14 | - Permalink copy, proxied download link copy. 15 | - Full dark mode support, style and website customisations. 16 | - Basically - All features completely refactored in the new project. 17 | 18 | Thank you for all the love and support! 19 | 20 | 250 | 251 | --- 252 | 253 | 🏵 **onedrive-cf-index** ©Spencer Woo. Released under the MIT License. 254 | 255 | > Authored and maintained by Spencer Woo. 256 | > 257 | > [@Portfolio](https://spencerwoo.com/) · [@Blog](https://blog.spencerwoo.com/) · [@GitHub](https://github.com/spencerwooo) 258 | -------------------------------------------------------------------------------- /src/render/favicon.js: -------------------------------------------------------------------------------- 1 | export const favicon = 2 | 'data:image/x-icon;base64,AAABAAMAEBAAAAEAIABoBAAANgAAACAgAAABACAAKBEAAJ4EAAAwMAAAAQAgAGgmAADGFQAAKAAAABAAAAAgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAC8AAAAAAAAAAAAAAKAwYXb/W6G+/1uhvv9bob7/W6G+/1uhvv9bob7/W6G+/1uhvv9bob7/W6G+/ypLWf8AAAB2AAAAAAAAAAAAAACgJXaZ/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//9Tk67/AAAAtgAAAAAAAAAAAAAAoAlqlP941v//etf//3rX//961///etf//3rX//961///etf//3rX//961///b8Xq/wIDBO8AAAAHAAAAAAAAAKAAZpP/Ys37/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//8WJy//AAAANwAAAAAAAACgAGaT/0TA9v961///etf//3rX//961///etf//3rX//961///etf//3rX//961///NF1u/wAAAHcAAAAAAAAAoABmk/8mtPL/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//1KSrv8AAAC3AAAAAAAAAKAAZpP/Cajt/3fW/v961///etf//3rX//961///etf//3rX//961///etf//3rX//9vxen/AQME7wAAAAcAAACgAGaT/wCk7P9jzfv/etf//3rX//961///etf//3rX//961///etf//3rX//961///etb+/xYnLv8AAAA4AAAAoAZRcf81oL//SajC/0yowv8dhrb/HYa2/x2Gtv8dhrb/HYa2/x2Gtv8dhrb/HVRr/x01QP8KEhX/AAAAdwAAAHoNCQP8oW4n/6dxKP+FWiD/AAAA7QAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAABwAAAAQAAAACgAAAAAAAAAaQAAAL8AAADBAAAAuAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAACAAAABAAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQAAAH8AAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAAAvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCAAAA/gAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAI4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEIAAAD+Cxkf/zxrfv88a37/PGt+/zxrfv88a37/PGt+/zxrfv88a37/PGt+/zxrfv88a37/PGt+/zxrfv88a37/PGt+/zxrfv88a37/PGt+/zxrfv88a37/PGt+/y9VZf8AAQH/AAAAywAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAQgAAAP4HLT3/ctT9/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///ccjt/wcNEP8AAADzAAAAGgAAAAAAAAAAAAAAAAAAAAAAAABCAAAA/gApPP9ayvr/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//951v7/HTQ//wAAAP4AAABPAAAAAAAAAAAAAAAAAAAAAAAAAEIAAAD+ACk7/zy+9f961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//87an7/AAAA/wAAAI4AAAAAAAAAAAAAAAAAAAAAAAAAQgAAAP4AKTv/HrHw/3nX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//1mfvf8AAAD/AAAAywAAAAMAAAAAAAAAAAAAAAAAAABCAAAA/gApO/8Hp+z/ctP+/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///cMfs/wcOEf8AAAD0AAAAGQAAAAAAAAAAAAAAAAAAAEIAAAD+ACk7/wCk6/9ayvr/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//951v7/HTQ+/wAAAP4AAABPAAAAAAAAAAAAAAAAAAAAQgAAAP4AKTv/AKTr/zy99f961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//86an3/AAAA/wAAAI8AAAAAAAAAAAAAAAAAAABCAAAA/gApO/8ApOv/HrHw/3nX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//1mfvP8AAAH/AAAAywAAAAMAAAAAAAAAAAAAAEIAAAD+ACk7/wCk6/8Hp+3/ctP9/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///ccfs/wcNEP8AAAD0AAAAGgAAAAAAAAAAAAAAQgAAAP4AKTv/AKTr/wCk7P9byvr/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//951f3/HTU//wAAAP4AAABPAAAAAAAAAAAAAABCAAAA/gApO/8ApOv/AKTs/zy+9f961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//86aX3/AAAA/wAAAI8AAAAAAAAAAAAAAEIAAAD+ACk7/wCk6/8ApOz/HrHw/3nX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//1ieu/8AAQH/AAAAzAAAAAMAAAAAAAAAQgAAAP4AKTv/AKTr/wCk7P8IqO3/cdP9/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///ccjs/wYMD/8AAADzAAAAGwAAAAAAAABCAAAA/gApO/8ApOv/AKTs/wCk7P9byvr/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//951f3/HTQ+/wAAAP8AAABQAAAAAAAAAEIAAAD+ACk7/wCk6/8ApOz/AKTs/zy+9f961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//87aXz/AAAA/wAAAI8AAAAAAAAAQgAAAP4AKTv/AKTs/wCk7P8ApOz/Eqzv/zy89f87vPX/O7z1/zu89f87vPX/O7z1/zu89f87vPX/O7z1/zu89f87vPX/O7z1/zu89f87vPX/O7z1/zu89f87vPX/O6jW/ztrgP87a4D/O2uA/yhIVv8AAAD/AAAAzQAAAAIAAABCAAAA/gAUHv8aZH7/a52T/2udk/9rnZP/a52T/2udk/9Qioz/AFF3/wBRd/8AUXf/AFF3/wBRd/8AUXf/AFF3/wBRd/8AUXf/AFF3/wBRd/8AUXf/AFF3/wBRd/8APFj/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD0AAAAGwAAAEIAAAD+AAAA/zEgC//cljb/3pc2/96XNv/elzb/3pc2/59rJv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA4AAAAIAAAACAAAAAgAAAAIAAAAAgAAAAIQAAAIYAAADzBQMB/1w+Fv9wTBr/cEwa/3BMGv9vSxr/KRsJ/wAAAP8AAAC5AAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHIAAAD5AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA1AAAABsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAADkAAAB7AAAAgwAAAIMAAACDAAAAggAAAGIAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAADAAAABgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAvAAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPgAAAD4AAAA+AAAAPwAAACQAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYAAAC1AAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAKMAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAANEAAAAhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wIEBf8UJy//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//x01P/8dNT//HTU//xsxO/8IDxL/AAAA/wAAAOkAAABEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wUPFP9FkbD/ccnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/3LJ7v9yye7/csnu/2zA4v8qTFr/AQIC/wAAAPYAAABtAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wIPFP83k7r/dtb+/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3jT+/9Acof/AwYH/wAAAPwAAACYAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8jirf/cNP9/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX/v9Ul7P/CA8S/wAAAP8AAADBAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Rg7T/Z8/7/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//9mtdf/EiEo/wAAAP8AAADkAAAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8FfrL/Vcj5/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//90zPL/IT5K/wAAAP8AAAD3AAAAXwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8BfLH/Obz1/3nX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961v7/PW6D/wAAAP8AAAD+AAAAmwAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8AfLH/HLHw/3bV/v961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///WaC9/wMHCf8AAAD/AAAA0gAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/B6jt/2jQ/P961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///bsTo/xAeJP8AAAD/AAAA8wAAADUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AaXs/03F+P961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///edT8/yhJWP8AAAD/AAAA/QAAAHAAAAACAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/zG58/931v7/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//0Z+lf8BAwT/AAAA/wAAAKsAAAAKAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/xiv8P9v0/3/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//12mxP8KExb/AAAA/wAAANQAAAAhAAAAAAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/w2p7v9fzPv/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//2u+4v8YLDX/AAAA/wAAAOkAAABFAAAAAQAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wan7f9LxPf/edf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3TM8v8sT13/AQEB/wAAAPYAAABvAAAABAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wKl7P83vPT/dtX+/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3jT+v9Ac4j/AwYH/wAAAP0AAACaAAAACwAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8js/H/cdP+/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rW/v9UlbH/CA8S/wAAAP8AAADCAAAAGQAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8RrO//Zs/8/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//9nttj/ER8l/wAAAP8AAADjAAAANAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8Fp+3/VMj5/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//90zPL/Ij5K/wAAAP8AAAD4AAAAYAAAAAAAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8ApOz/Or31/3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//951fz/PG2B/wABAf8AAAD/AAAAnAAAAAIAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8ApOz/HbHx/3XV/v961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///WaC+/wMHCP8AAAD/AAAA0QAAABMAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8ApOz/Cajt/2fQ/P961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///b8bq/w8cIv8AAAD/AAAA8gAAADgAAAAAAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8ApOz/AaXs/07F+P961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///eNP7/yhIVv8AAAD/AAAA/gAAAHIAAAABAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8ApOz/AKTs/zC59P931v7/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//0V9lP8CBAX/AAAA/wAAAKsAAAALAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8ApOz/AKTs/xuv8P9v0/3/etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//3rX//961///etf//16nxv8IEBT/AAAA/wAAANMAAAAiAAAAAAAAABgAAADAAAAA/wAOFP8Ae7H/AKTs/wCk7P8ApOz/AKTs/wup7f9Jw/f/Wsn6/1rJ+v9ayfr/Wsn6/1rJ+v9ayfr/Wsn6/1rJ+v9ayfr/Wsn6/1rJ+v9ayfr/Wsn6/1rJ+v9ayfr/Wsn6/1rJ+v9ayfr/Wsn6/1rJ+v9ayfr/Wsn6/1rJ+v9ayfr/Wsn6/1rG9P9aq83/WqG//1qhv/9aob//WqG//0+NqP8PHCL/AAAA/wAAAOoAAABGAAAAAAAAABgAAADAAAAA/wANE/8AdKj/BJ7h/wmj4v8Jo+L/CaPi/wqj4/8PpeP/EKXj/xCl4/8QpeP/DKLi/wed4P8HneD/B53g/wed4P8HneD/B53g/wed4P8HneD/B53g/wed4P8HneD/B53g/wed4P8HneD/B53g/wed4P8HneD/B53g/wed4P8HneD/B53g/weRzf8HMEL/Bw0Q/wcNEP8HDRD/Bw0Q/wYMD/8BAwP/AAAA/wAAAPYAAABwAAAABAAAABgAAADAAAAA/wADBf8AHiz/QlZM/6WaZf+lmmX/pZpl/6WaZf+lmmX/pZpl/6WaZf+mmmX/X2lS/wAnO/8AJzv/ACc7/wAnO/8AJzv/ACc7/wAnO/8AJzv/ACc7/wAnO/8AJzv/ACc7/wAnO/8AJzv/ACc7/wAnO/8AJzv/ACc7/wAnO/8AJzv/ACc7/wAkNf8ACQ7/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPwAAACZAAAACwAAABgAAADAAAAA/wAAAP8AAAD/VjkU/9yWNv/elzb/3pc2/96XNv/elzb/3pc2/96XNv/elzb/fFMc/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPoAAADRAAAAwQAAAMEAAADBAAAAwQAAAMEAAACPAAAAEQAAABYAAAC0AAAA8QAAAP4AAAD/LR4K/7h8Lf/QjTP/0Y4z/9GOM//RjjP/0Y4z/9GOM//Bgi7/SjEQ/wAAAP8AAAD+AAAA9AAAAO8AAADvAAAA7wAAAO8AAADvAAAA7wAAAO8AAADvAAAA7wAAAO8AAADvAAAA7wAAAO8AAADvAAAA7wAAAO8AAADvAAAA7wAAAO8AAADvAAAA7wAAANwAAABIAAAADwAAAA8AAAAPAAAADwAAAA8AAAAMAAAAAQAAAAYAAAAxAAAAUAAAAN8AAAD/AwIB/yQYCP84Jg3/OCYN/zgmDf84Jg3/OCYN/zgmDf8rHQn/CAUB/wAAAP8AAADsAAAAZwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAGgAAADrAAAA/gAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPYAAACXAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAABoAAAAxQAAAOsAAADxAAAA8gAAAPIAAADyAAAA8gAAAPIAAADtAAAA0wAAAIcAAAAiAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAHAAAADYAAABCAAAAQwAAAEMAAABDAAAAQwAAAEIAAAA6AAAAIgAAAAkAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==' 3 | --------------------------------------------------------------------------------