first commit

This commit is contained in:
pscgyLancer 2022-05-14 11:16:58 +08:00
commit 5f613c0105
38 changed files with 6392 additions and 0 deletions

21
.eslintrc.js Normal file
View File

@ -0,0 +1,21 @@
module.exports = {
env: {
browser: true,
es2020: true
},
extends: ['standard'],
parserOptions: {
ecmaVersion: 11,
sourceType: 'module'
},
rules: {
'space-before-function-paren': ['error', 'never']
},
globals: {
TransformStream: true,
REFRESH_TOKEN: true,
CLIENT_SECRET: true,
BUCKET: true,
AUTH_PASSWORD: true
}
}

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
/target
/dist
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log
worker/
node_modules/
.cargo-ok

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"tabWidth": 2,
"printWidth": 120
}

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at ag_dubs@cloudflare.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

25
LICENSE Normal file
View File

@ -0,0 +1,25 @@
Copyright (c) 2018 Ashley Williams <ashley666ashley@gmail.com>
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.

235
README-CN.md Normal file
View File

@ -0,0 +1,235 @@
<div align="center">
<image src="assets/onedrive-cf-index.png" alt="onedrive-cf-index" width="150px" />
<h3><a href="https://storage.spencerwoo.com">onedrive-cf-index</a></h3>
<em>由 CloudFlare Workers 强力驱动的 OneDrive 索引</em>
</div>
---
[![Hosted on Cloudflare Workers](https://img.shields.io/badge/Hosted%20on-CF%20Workers-f38020?logo=cloudflare&logoColor=f38020&labelColor=282d33)](https://storage.spencerwoo.com/)
[![Deploy](https://github.com/spencerwooo/onedrive-cf-index/workflows/Deploy/badge.svg)](https://github.com/spencerwooo/onedrive-cf-index/actions?query=workflow%3ADeploy)
[![README-CN](assets/chinese.svg)](./README-CN.md)
<h5>本项目使用 CloudFlare Workers 帮助你免费部署与分享你的 OneDrive 文件。本项目极大源自:<a href="https://github.com/heymind/OneDrive-Index-Cloudflare-Worker">onedrive-index-cloudflare-worker</a>,致敬。</h5>
## Demo
在线演示:[Spencer's OneDrive Index](https://storage.spencerwoo.com/).
![Screenshot Demo](assets/screenshot.png)
## 功能
### 🚀 功能一览
- 全新「面包屑」导航栏;
- 令牌凭证由 Cloudflare Workers 自动刷新,并保存于(免费的)全局 KV 存储中;
- 使用 [Turbolinks®](https://github.com/turbolinks/turbolinks) 实现路由懒加载;
- 支持由世纪互联运营的 OneDrive 版本;
- 支持 SharePoint 部署;
### 🗃️ 目录索引显示
- 全新支持自定义的设计风格:[spencer.css](themes/spencer.css)
- 支持使用 Emoji 作为文件夹图标(如果文件夹名称第一位是 Emoji 则自动开启该功能);
- 渲染 `README.md` 如果当前目录下包含此文件,使用 [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) 渲染样式;
- 支持「分页」,没有一个目录仅限显示 200 个项目的限制了!
### 📁 文件在线预览
- 根据文件类型渲染文件图标,图标使用 [Font Awesome icons](https://fontawesome.com/)
- 支持预览:
- 纯文本:`.txt`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/iso_8859-1.txt).
- Markdown 格式文本:`.md`, `.mdown`, `.markdown`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/i_m_a_md.md).
- 图片(支持 Medium 风格的图片缩放):`.png`, `.jpg`, and `.gif`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/).
- 代码高亮:`.js`, `.py`, `.c`, `.json`... [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Code/pathUtil.js).
- PDF支持懒加载、加载进度、Chrome 内置 PDF 阅读器):`.pdf`. [_DEMO_](<https://storage.spencerwoo.com/%F0%9F%A5%91%20Course%20PPT%20for%20CS%20(BIT)/2018%20-%20%E5%A4%A7%E4%BA%8C%E4%B8%8B%20-%20%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/1%20FoundationofCG-Anonymous.pdf>).
- 音乐:`.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).
- 视频:`.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).
### 🔒 私有文件夹
![Private folders](assets/private-folder.png)
我们可以给某个特定的文件夹(目录)上锁,需要认证才能访问。我们可以在 `src/auth/config.js` 文件中将我们想要设为私有文件夹的目录写入 `ENABLE_PATHS` 列表中。我们还可以自定义认证所使用的用户名 `NAME` 以及密码,其中认证密码保存于 `AUTH_PASSWORD` 环境变量中,需要使用 wrangler 来设置这一环境变量:
```bash
wrangler secret put AUTH_PASSWORD
# 在这里输入你自己的认证密码
```
有关 wrangler 的使用细节等详细内容,请参考 [接下来的部分段落](#准备工作)。
### ⬇️ 代理下载文件 / 文件直链访问
- [可选] Proxied download代理下载文件`?proxied` - 经由 CloudFlare Workers 下载文件如果1`config/default.js` 中的 `proxyDownload``true`以及2使用参数 `?proxied` 请求文件;
- [可选] Raw file download文件直链访问`?raw` - 返回文件直链而不是预览界面;
- 两个参数可以一起使用,即 `?proxied&raw``?raw&proxied` 均有效。
是的,这也就意味着你可以将这一项目用来搭建「图床」,或者用于搭建静态文件部署服务,比如下面的图片链接:
```
https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/nyancat.gif?raw
```
![](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/nyancat.gif?raw)
### 其他功能
请参考原项目的「🔥 新特性 V1.1」部分:[onedrive-index-cloudflare-worker](https://github.com/heymind/OneDrive-Index-Cloudflare-Worker#-%E6%96%B0%E7%89%B9%E6%80%A7-v11)**但我不保证全部功能均可用,因为本项目改动部分很大。**
## 部署指南
_又臭又长的中文版部署指南预警_
### 生成 OneDrive API 令牌
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 世纪互联版本),**建议将语言设置为「英语」以保证以下步骤中提到的模块和按钮的名称一致**
1. 使用你的 Microsoft 账户登录,选择 `New registration`
2. 在 `Name` 处设置 Blade app 的名称,比如 `my-onedrive-cf-index`
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 目录 - 多租户)中的帐户`
4. 将 `Redirect URI (optional)` 设置为 `Web`(下拉选项框)以及 `https://localhost`URL 地址);
5. 点击 `Register`.
![](assets/register-app.png)
2. 在 `Overview` 面板获取你的 Application (client) ID - `client_id`
![](assets/client-id.png)
3. 打开 `Certificates & secrets` 面板,点击 `New client secret`,创建一个新的叫做 `client_secret` 的 Client secret并将 `Expires` 设置为 `Never`。点击 `Add` 并复制 `client_secret``Value` 并保存下来 **(仅有此一次机会)**
![](assets/add-client-secret.png)
4. 打开 `API permissions` 面板,选择 `Microsoft Graph`,选择 `Delegated permissions`,并搜索 `offline_access, Files.Read, Files.Read.All` 这三个权限,**选择这三个权限,并点击 `Add permissions`**
![](assets/add-permissions.png)
你应该成功开启这三个权限:
![](assets/permissions-used.png)
5. 获取 `refresh_token`,在本机(需要 Node.js 和 npm 环境,安装和推荐配置请参考 [准备工作](#准备工作))上面执行如下命令:
```sh
npx @beetcb/ms-graph-cli
```
<div align="center"><img src="https://raw.githubusercontent.com/beetcb/ms-graph-cli/master/media/demo.svg" alt="demo gif" width="560px" /></div>
根据你自己的情况选择合适的选项,并输入我们上面获取到的一系列 token 令牌配置等,其中 `redirect_url` 可以直接设置为 `http://localhost`。有关命令行工具的具体使用方法请参考:[beetcb/ms-graph-cli](https://github.com/beetcb/ms-graph-cli)。
6. 最后,在我们的 OneDrive 中创建一个公共分享文件夹,比如 `/Public` 即可。建议不要直接分享根目录!
最后,这么折腾完,我们应该成功拿到如下的几个凭证:
- `refresh_token`
- `client_id`
- `client_secret`
- `redirect_uri`
- `base`:默认为 `/Public`
_是我知道很麻烦但是这是微软大家理解一下。🤷🏼_
### 准备工作
Fork 再 clone 或者直接 clone 本仓库,并安装依赖 Node.js、`npm` 以及 `wrangler`
_强烈建议大家使用 Node version manager 比如 [n](https://github.com/tj/n) 或者 [nvm](https://github.com/nvm-sh/nvm) 安装 Node.js 和 `npm`,这样我们全局安装的 `wrangler` 就可以在我们的用户目录下安装保存配置文件了也就不会遇到奇奇怪怪的权限问题了。_
```sh
# 安装 CloudFlare Workers 官方编译部署工具
npm i @cloudflare/wrangler -g
# 使用 npm 安装依赖
npm install
# 使用 wrangler 登录 CloudFlare 账户
wrangler login
# 使用这一命令检查自己的登录状态
wrangler whoami
```
打开 <https://dash.cloudflare.com/login> 登录 CloudFlare选择自己的域名**再向下滚动一点,我们就能看到右侧栏处我们的 `account_id` 以及 `zone_id` 了。** 同时,在 `Workers` -> `Manage Workers` -> `Create a Worker` 处创建一个 **DRAFT** worker。
修改我们的 [`wrangler.toml`](wrangler.toml)
- `name`:就是我们刚刚创建的 draft worker 名称,我们的 Worker 默认会发布到这一域名下:`<name>.<worker_subdomain>.workers.dev`
- `account_id`:我们的 Cloudflare Account ID
- `zone_id`:我们的 Cloudflare Zone ID。
创建叫做 `BUCKET` 的 Cloudflare Workers KV bucket
```sh
# 创建 KV bucket
wrangler kv:namespace create "BUCKET"
# ... 或者,创建包括预览功能的 KV bucket
wrangler kv:namespace create "BUCKET" --preview
```
修改 [`wrangler.toml`](wrangler.toml) 里面的 `kv_namespaces`
- `kv_namespaces`:我们的 Cloudflare KV namespace仅需替换 `id` 和(或者)`preview_id` 即可。_如果不需要预览功能那么移除 `preview_id` 即可。_
修改 [`src/config/default.js`](src/config/default.js)
- `client_id`:刚刚获取的 OneDrive `client_id`
- `base`:之前创建的 `base` 目录;
- 如果你部署常规国际版 OneDrive那么忽略以下步骤即可
- 如果你部署的是由世纪互联运营的中国版 OneDrive
- 修改 `type` 下的 `accountType``1`
- 保持 `driveType` 不变;
- 如果你部署的是 SharePoint 服务:
- 保持 `accountType` 不变;
- 修改 `driveType` 下的 `type``1`
- 并根据你的 SharePoint 服务修改 `hostName``sitePath`
使用 `wrangler` 添加 Cloudflare Workers 环境变量(有关认证密码的介绍请见 [🔒 私有文件夹](#-私有文件夹)
```sh
# 添加我们的 refresh_token 和 client_secret
wrangler secret put REFRESH_TOKEN
# ... 并在这里粘贴我们的 refresh_token
wrangler secret put CLIENT_SECRET
# ... 并在这里粘贴我们的 client_secret
wrangler secret put AUTH_PASSWORD
# ... 在这里输入我们自己设置的认证密码
```
### 编译与部署
我们可以使用 `wrangler` 预览部署:
```sh
wrangler preview
```
如果一切顺利,我们即可使用如下命令发布 Cloudflare Worker
```sh
wrangler publish
```
我们也可以创建一个 GitHub Actions 来在每次 `push` 到 GitHub 仓库时自动发布新的 Worker详情参考[main.yml](.github/workflows/main.yml)。
如果想在自己的域名下部署 Cloudflare Worker请参考[How to Setup Cloudflare Workers on a Custom Domain](https://www.andressevilla.com/how-to-setup-cloudflare-workers-on-a-custom-domain/)。
## 样式、内容的自定义
- 我们 **应该** 更改默认「着落页面」,直接修改 [src/folderView.js](src/folderView.js#L51-L55) 中 `intro` 的 HTML 即可;
- 我们也 **应该** 更改页面的 header直接修改 [src/render/htmlWrapper.js](src/render/htmlWrapper.js#L24) 即可;
- 样式 CSS 文件位于 [themes/spencer.css](themes/spencer.css),可以根据自己需要自定义此文件,同时也需要更新 [src/render/htmlWrapper.js](src/render/htmlWrapper.js#L3) 文件中的 commit HASH
- 我们还可以自定义 Markdown 渲染 CSS 样式、PrismJS 代码高亮样式,等等等。
---
🏵 **onedrive-cf-index** ©Spencer Woo. Released under the MIT License.
Authored and maintained by Spencer Woo.
[@Portfolio](https://spencerwoo.com/) · [@Blog](https://blog.spencerwoo.com/) · [@GitHub](https://github.com/spencerwooo)

236
README.md Normal file
View File

@ -0,0 +1,236 @@
<div align="center">
<image src="assets/onedrive-cf-index.png" alt="onedrive-cf-index" width="150px" />
<h3><a href="https://storage.spencerwoo.com">onedrive-cf-index</a></h3>
<em>Yet another OneDrive index, powered by CloudFlare Workers.</em>
</div>
---
[![Hosted on Cloudflare Workers](https://img.shields.io/badge/Hosted%20on-CF%20Workers-f38020?logo=cloudflare&logoColor=f38020&labelColor=282d33)](https://storage.spencerwoo.com/)
[![Deploy](https://github.com/spencerwooo/onedrive-cf-index/workflows/Deploy/badge.svg)](https://github.com/spencerwooo/onedrive-cf-index/actions?query=workflow%3ADeploy)
[![README-CN](assets/chinese.svg)](./README-CN.md)
<h5>This project uses CloudFlare Workers to host and share your personal OneDrive files. It is greatly inspired by: <a href="https://github.com/heymind/OneDrive-Index-Cloudflare-Worker">onedrive-index-cloudflare-worker</a>.</h5>
## Demo
Live demo at [Spencer's OneDrive Index](https://storage.spencerwoo.com/).
![Screenshot Demo](assets/screenshot.png)
## Features
### 🚀 General
- Breadcrumbs for better navigations.
- Tokens cached and automatically refreshed with Cloudflare Workers KV storage.
- Route lazy loading with the help of [Turbolinks®](https://github.com/turbolinks/turbolinks).
- Supports OneDrive 21Vianet.(由世纪互联运营的 OneDrive。
- Supports mounting SharePoint.
### 🗃️ Folder indexing
- Complete new and customisable design at [spencer.css](themes/spencer.css).
- Emoji as folder icon when available (if the first character of the folder name is an emoji).
- Renders `README.md` if found in current folder. Rendered with [github-markdown-css](https://github.com/sindresorhus/github-markdown-css).
- Supports pagination, no more limitations like 200 items max for each folder ever again!
### 📁 File previews
- File icon rendered according to file type, [Font Awesome icons](https://fontawesome.com/) for cleaner look.
- Plain text: `.txt`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/iso_8859-1.txt).
- Markdown: `.md`, `.mdown`, `.markdown`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/i_m_a_md.md).
- Image, supports Medium style zoom effect: `.png`, `.jpg`, and `.gif`. [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Previews/).
- Code with syntax highlighting: `.js`, `.py`, `.c`, `.json`.... [_DEMO_](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/Code/pathUtil.js).
- PDF: Lazy loading, loading progress and built-in PDF viewer. [_DEMO_](<https://storage.spencerwoo.com/%F0%9F%A5%91%20Course%20PPT%20for%20CS%20(BIT)/2018%20-%20%E5%A4%A7%E4%BA%8C%E4%B8%8B%20-%20%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/1%20FoundationofCG-Anonymous.pdf>).
- Music / Audio: `.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).
- Videos: `.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).
### ⬇️ Proxied / raw file download
- [Optional] Proxied download: `?proxied` - Downloads the file through CloudFlare Workers if (1) `proxyDownload` is true in `config/default.js` and (2) parameter is present in url.
- [Optional] Raw file download: `?raw` - Return direct raw file instead of rich rendered preview if parameter is present.
- Both these parameters can be used side by side, meaning that `?proxied&raw` and `?raw&proxied` are both valid.
Yes, this means you can use this project as an image storage service or for serving static files, for example:
```
https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/nyancat.gif?raw
```
![](https://storage.spencerwoo.com/%F0%9F%A5%9F%20Some%20test%20files/nyancat.gif?raw)
### 🔒 Private folders
![Private folders](assets/private-folder.png)
You can limit access to folders (i.e., declaring private folders) by adding their paths to `ENABLE_PATHS` inside `src/auth/config.js`. You can optionally enable this feature with the `AUTH_ENABLED` toggle variable also inside that file, and you can specify the username in `NAME` and the password using wrangler.
Note that the password is stored inside the `AUTH_PASSWORD` Cloudflare Worker secret. You should never commit your password into a git repository, not even a private one. The `AUTH_PASSWORD` secret can be added with wrangler:
```bash
wrangler secret put AUTH_PASSWORD
# Type out your self-defined AUTH_PASSWORD here
```
Check out [the following sections](#preparations) for details on using wrangler to set CloudFlare Worker secrets (which are also called environment variables).
### Others
See the new features section at the original [onedrive-index-cloudflare-worker](https://github.com/heymind/OneDrive-Index-Cloudflare-Worker#-%E6%96%B0%E7%89%B9%E6%80%A7-v11) project page for reference, **although I cannot guarantee that all features are usable.**
## Deployment
_Very, very long, tedious, step by step guide warning!_
### Generating OneDrive API Tokens
1. Create a new blade app here [Microsoft Azure App registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) (OneDrive normal version) or [Microsoft Azure.cn App registrations](https://portal.azure.cn/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) (OneDrive 世纪互联版本):
1. Login with your Microsoft account, select `New registration`.
2. Input `Name` for your blade app, `my-onedrive-cf-index` for example.
3. Set `Supported account types` to `Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)`. OneDrive 世纪互联用户设置为:`任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户`.
4. Set `Redirect URI (optional)` to `Web` (the multiselect dropdown) and `http://localhost` (the URL).
5. Click `Register`.
![](assets/register-app.png)
2. Get your Application (client) ID - `client_id` at `Overview` panel.
![](assets/client-id.png)
3. Open `Certificates & secrets` panel, click `New client secret` and create a new secret called `client_secret`, set `Expires` to `Never`, click `Add`, and copy the `Value` of the `client_secret` (**You only have this one chance to copy it.**).
![](assets/add-client-secret.png)
4. Open `API permissions` panel, select `Microsoft Graph`, select `Delegated permissions`, search for `offline_access, Files.Read, Files.Read.All`, **select all three of them** and click `Add permissions`.
![](assets/add-permissions.png)
You should have these permissions ready:
![](assets/permissions-used.png)
5. Get your `refresh_token`. On your local machine that has a working installation of Node.js and npm (See [Preparations](#preparations) for recommendations for installing Node.js and its toolchain), execute the following command:
```sh
npx @beetcb/ms-graph-cli
```
<div align="center"><img src="https://raw.githubusercontent.com/beetcb/ms-graph-cli/master/media/demo.svg" alt="demo gif" width="560px" /></div>
Select the options that you need, and enter the tokens that we just acquired from above. The names are self-explanatory. `redirect_url` can be set to `http://localhost`. For more information please go check out the repo at: [beetcb/ms-graph-cli](https://github.com/beetcb/ms-graph-cli).
6. Finally, create a dedicated folder for your public files inside OneDrive, for instance: `/Public`. Please don't share your root folder directly!
After all this hassle, you should have successfully acquired the following tokens and secrets:
- `refresh_token`
- `client_id`
- `client_secret`
- `redirect_uri`
- `base`: Defaults to `/Public`.
_Yes, I know it's a long and tedious procedure, but it's Microsoft, we can understand. 🤷🏼_
### Preparations
Fork then clone, or directly clone this repository. Install dependencies locally, you'll need Node.js, `npm` and `wrangler`.
_We strongly recommend you install npm with a Node version manager like [n](https://github.com/tj/n) or [nvm](https://github.com/nvm-sh/nvm), which will allow wrangler to install configuration data in a global node_modules directory in your user's home directory, without requiring that you run as root._
```sh
# Install cloudflare workers official packing and publishing tool
npm i @cloudflare/wrangler -g
# Install dependencies with npm
npm install
# Login to Cloudflare with wrangler
wrangler login
# Verify wrangler status with this command
wrangler whoami
```
Login to your CloudFlare account at <https://dash.cloudflare.com/login>, select your domain, **and scroll down a bit. You'll see your `account_id` and `zone_id` there (on the right sidebar).** Also create a **DRAFT** worker at `Workers` -> `Manage Workers` -> `Create a Worker` with a cool name.
Modify [`wrangler.toml`](wrangler.toml):
- `name`: The draft worker's name, your worker will be published at `<name>.<worker_subdomain>.workers.dev`.
- `account_id`: Your Cloudflare Account ID.
- `zone_id`: Your Cloudflare Zone ID.
Create Cloudflare Workers KV bucket named `BUCKET`:
```sh
# Create KV bucket
wrangler kv:namespace create "BUCKET"
# ... or, create KV bucket with preview functions enabled
wrangler kv:namespace create "BUCKET" --preview
```
Modify `kv_namespaces` inside [`wrangler.toml`](wrangler.toml):
- `kv_namespaces`: Your Cloudflare KV namespace, you should substitute the `id` and `preview_id` values accordingly. _If you don't need preview functions, you can remove the `preview_id` field._
Modify [`src/config/default.js`](src/config/default.js):
- `client_id`: Your `client_id` from above.
- `base`: Your `base` path from above.
- If you are mounting regular international OneDrive, you can safely ignore the following steps.
- If you are mounting Chinese 21Vianet OneDrive (由世纪互联运营的 OneDrive):
- Set `accountType` under `type` to `1`.
- Keep `driveType` unmodified.
- If you are mounting SharePoint:
- Keep `accountType` unmodified.
- Set `driveType` under `type` to `1`.
- Set `hostName` and `sitePath` accordingly.
Add secrets to Cloudflare Workers environment variables with `wrangler` (For `AUTH_PASSWORD` and private folders, refer to [🔒 Private folders](#-private-folders)):
```sh
# Add your refresh_token and client_secret to Cloudflare
wrangler secret put REFRESH_TOKEN
# ... enter your refresh_token from above here
wrangler secret put CLIENT_SECRET
# ... enter your client_secret from above here
wrangler secret put AUTH_PASSWORD
# Type out your self-defined AUTH_PASSWORD here
```
### Building and deployment
You can preview the worker with `wrangler`:
```sh
wrangler preview
```
After making sure everything is ok, you can publish your worker with:
```sh
wrangler publish
```
You can also create a GitHub Actions for auto publishing your worker on `push`. See [main.yml](.github/workflows/main.yml).
For custom domains, refer to [How to Setup Cloudflare Workers on a Custom Domain](https://www.andressevilla.com/how-to-setup-cloudflare-workers-on-a-custom-domain/).
## Customisations
- You can **(AND SHOULD)** change the `intro` on the default landing page here: [src/folderView.js](src/folderView.js#L51-L55). Write HTML directly.
- You can **(AND ALSO SHOULD)** change the header of the site here: [src/render/htmlWrapper.js](src/render/htmlWrapper.js#L24).
- Your custom styles are loaded from [themes/spencer.css](themes/spencer.css), change that according to your customizations. You will also need to change the commit HASH at [src/render/htmlWrapper.js](src/render/htmlWrapper.js#L3).
- You can also customize Markdown CSS styles, PrismJS code highlight color schemes, etc.
---
🏵 **onedrive-cf-index** ©Spencer Woo. Released under the MIT License.
Authored and maintained by Spencer Woo.
[@Portfolio](https://spencerwoo.com/) · [@Blog](https://blog.spencerwoo.com/) · [@GitHub](https://github.com/spencerwooo)

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
assets/add-permissions.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

23
assets/chinese.svg Normal file
View File

@ -0,0 +1,23 @@
<svg width="60" height="20" viewBox="0 0 60 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="20">
<path d="M57 0H3C1.34315 0 0 1.34315 0 3V17C0 18.6569 1.34315 20 3 20H57C58.6569 20 60 18.6569 60 17V3C60 1.34315 58.6569 0 57 0Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M57 0H3C1.34315 0 0 1.34315 0 3V17C0 18.6569 1.34315 20 3 20H57C58.6569 20 60 18.6569 60 17V3C60 1.34315 58.6569 0 57 0Z" fill="#2B2B2B"/>
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="22" height="20">
<path d="M22 0H0V20H22V0Z" fill="white"/>
</mask>
<g mask="url(#mask1)">
<path d="M22 0H0V20H22V0Z" fill="#F8E81C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 -3H25V22H0V-3ZM6.41563 10.3823L9.28333 12.5666L8.20729 9.14372L11.0115 6.96038H7.48958L6.41563 3.60309L5.33958 6.96038H1.81875L4.62292 9.14372L3.54583 12.5666L6.41563 10.3823ZM18.0323 12.4593L18.976 13.1771L18.6219 12.0521L19.5448 11.3333H18.3854L18.0323 10.2281L17.6781 11.3333H16.5198L17.4427 12.0521L17.0885 13.1771L18.0323 12.4593ZM14.4094 15.8166L15.026 16.8312L15.1094 15.6531L16.2313 15.325L15.1531 14.9L15.2292 13.7416L14.4948 14.6406L13.4167 14.2156L14.0115 15.2218L13.2698 16.1385L14.4094 15.8166ZM18.3792 7.38226L19.549 7.58226L18.7146 6.74788L19.2 5.68434L18.1708 6.2208L17.3469 5.40413L17.5448 6.54788L16.5177 7.08226L17.6677 7.29268L17.8729 8.45518L18.3792 7.38226ZM15.1781 3.8833L16.3385 3.64893L15.2604 3.17184L15.3302 2.00518L14.5646 2.87497L13.501 2.41143L14.0979 3.40726L13.3333 4.27601L14.4812 4.05726L15.0938 5.06663L15.1781 3.8833Z" fill="#D0011B"/>
</g>
</g>
<rect width="60" height="20" rx="3" fill="url(#paint0_linear)"/>
<path d="M35.432 11.148V7.968H38.876V11.148H35.432ZM31.076 11.148V7.968H34.508V11.148H31.076ZM35.432 7.092V4.956H34.508V7.092H30.2V12.768H31.076V12.024H34.508V15.912H35.432V12.024H38.876V12.708H39.788V7.092H35.432ZM47.936 6.732C47.768 6.228 47.36 5.46 47 4.896L46.076 5.16C46.436 5.748 46.82 6.552 46.964 7.044L47.936 6.732ZM49.508 7.944C48.908 9.564 48.104 10.884 47.036 11.964C45.908 10.836 45.044 9.468 44.42 7.944H49.508ZM52.4 7.944V7.068H41.648V7.944H43.496C44.192 9.756 45.14 11.316 46.376 12.588C45.068 13.692 43.46 14.508 41.48 15.072C41.66 15.276 41.924 15.684 42.032 15.9C44.024 15.264 45.68 14.4 47.024 13.224C48.356 14.424 49.988 15.3 51.944 15.852C52.088 15.6 52.34 15.228 52.556 15.024C50.648 14.556 49.028 13.704 47.708 12.588C48.908 11.352 49.832 9.84 50.516 7.944H52.4Z" fill="#FDFDFD"/>
<defs>
<linearGradient id="paint0_linear" x1="28" y1="-1.92324e-06" x2="28" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#EEEEEE" stop-opacity="0.1"/>
<stop offset="1" stop-opacity="0.1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/client-id.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

31
assets/english.svg Normal file
View File

@ -0,0 +1,31 @@
<svg width="71" height="20" viewBox="0 0 71 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="71" height="20">
<path d="M68 0H3C1.34315 0 0 1.34315 0 3V17C0 18.6569 1.34315 20 3 20H68C69.6569 20 71 18.6569 71 17V3C71 1.34315 69.6569 0 68 0Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M68 0H3C1.34315 0 0 1.34315 0 3V17C0 18.6569 1.34315 20 3 20H68C69.6569 20 71 18.6569 71 17V3C71 1.34315 69.6569 0 68 0Z" fill="#1056A7"/>
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="22" height="20">
<path d="M22 0H0V20H22V0Z" fill="white"/>
</mask>
<g mask="url(#mask1)">
<path d="M22 0H0V20H22V0Z" fill="#CB0000"/>
<mask id="mask2" mask-type="alpha" maskUnits="userSpaceOnUse" x="-6" y="0" width="39" height="20">
<path d="M28 0H-1C-3.76142 0 -6 2.23858 -6 5V15C-6 17.7614 -3.76142 20 -1 20H28C30.7614 20 33 17.7614 33 15V5C33 2.23858 30.7614 0 28 0Z" fill="white"/>
</mask>
<g mask="url(#mask2)">
<path d="M28 0H-1C-3.76142 0 -6 2.23858 -6 5V15C-6 17.7614 -3.76142 20 -1 20H28C30.7614 20 33 17.7614 33 15V5C33 2.23858 30.7614 0 28 0Z" fill="#2F80B7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2794 8.59339L28.3094 0.559131L29.4249 0L30.5364 2.2449L29.4208 2.80403L15.0747 9.99436L29.4433 17.196L30.5589 17.7551L29.4475 20L28.3319 19.4409L12.2794 11.3953L-3.77301 19.4409L-4.88858 20L-6 17.7551L-4.88442 17.196L9.48422 9.99436L-4.86192 2.80403L-5.9775 2.2449L-4.86608 0L-3.7505 0.559131L12.2794 8.59339Z" fill="#BE3B31" stroke="#EDF1F2"/>
<path d="M10.1269 9.10601V0H12.1269V9.10601H28.2353V11.106H12.1269V20H10.1269V11.106H-5V9.10601H10.1269Z" fill="#BE3B31"/>
<path d="M10.1269 9.10601V0H12.1269V9.10601H28.2353V11.106H12.1269V20H10.1269V11.106H-5V9.10601H10.1269Z" stroke="#EDF1F2" stroke-width="2"/>
</g>
</g>
</g>
<rect width="71" height="20" rx="3" fill="url(#paint0_linear)"/>
<path d="M27.3094 15H32.5174V14.052H28.4254V10.848H31.7614V9.9H28.4254V7.14H32.3854V6.192H27.3094V15ZM34.2681 15H35.3601V10.272C36.0201 9.612 36.4761 9.276 37.1481 9.276C38.0121 9.276 38.3841 9.792 38.3841 11.016V15H39.4761V10.872C39.4761 9.204 38.8521 8.316 37.4841 8.316C36.5961 8.316 35.9241 8.808 35.3001 9.42H35.2641L35.1681 8.484H34.2681V15ZM43.8 18C45.816 18 47.1 16.956 47.1 15.744C47.1 14.676 46.332 14.196 44.82 14.196H43.548C42.672 14.196 42.408 13.908 42.408 13.488C42.408 13.128 42.588 12.912 42.828 12.708C43.116 12.852 43.476 12.936 43.8 12.936C45.12 12.936 46.176 12.06 46.176 10.668C46.176 10.104 45.96 9.624 45.648 9.324H46.98V8.484H44.712C44.484 8.4 44.16 8.316 43.8 8.316C42.48 8.316 41.352 9.216 41.352 10.644C41.352 11.424 41.772 12.048 42.204 12.396V12.444C41.868 12.684 41.472 13.116 41.472 13.656C41.472 14.172 41.724 14.52 42.072 14.724V14.772C41.46 15.168 41.112 15.696 41.112 16.248C41.112 17.376 42.204 18 43.8 18ZM43.8 12.192C43.044 12.192 42.408 11.592 42.408 10.644C42.408 9.684 43.032 9.12 43.8 9.12C44.568 9.12 45.192 9.696 45.192 10.644C45.192 11.592 44.544 12.192 43.8 12.192ZM43.956 17.256C42.78 17.256 42.072 16.8 42.072 16.104C42.072 15.72 42.264 15.336 42.732 15C43.02 15.072 43.332 15.108 43.572 15.108H44.7C45.564 15.108 46.032 15.312 46.032 15.912C46.032 16.596 45.204 17.256 43.956 17.256ZM49.5177 15.156C49.8177 15.156 49.9977 15.12 50.1537 15.06L49.9977 14.22C49.8777 14.244 49.8297 14.244 49.7697 14.244C49.6017 14.244 49.4577 14.112 49.4577 13.776V5.448H48.3657V13.704C48.3657 14.628 48.7017 15.156 49.5177 15.156ZM51.7876 15H52.8796V8.484H51.7876V15ZM52.3396 7.14C52.7716 7.14 53.0716 6.852 53.0716 6.408C53.0716 5.988 52.7716 5.7 52.3396 5.7C51.9076 5.7 51.6076 5.988 51.6076 6.408C51.6076 6.852 51.9076 7.14 52.3396 7.14ZM56.7963 15.156C58.3323 15.156 59.1723 14.28 59.1723 13.224C59.1723 11.988 58.1283 11.604 57.1803 11.244C56.4483 10.968 55.7763 10.74 55.7763 10.116C55.7763 9.612 56.1603 9.18 56.9883 9.18C57.5643 9.18 58.0203 9.42 58.4643 9.756L58.9923 9.048C58.4883 8.652 57.7803 8.316 56.9763 8.316C55.5603 8.316 54.7203 9.132 54.7203 10.164C54.7203 11.268 55.7163 11.712 56.6283 12.048C57.3483 12.324 58.1163 12.612 58.1163 13.284C58.1163 13.848 57.6963 14.304 56.8323 14.304C56.0523 14.304 55.4643 13.992 54.9003 13.536L54.3603 14.256C54.9843 14.772 55.8723 15.156 56.7963 15.156ZM60.7056 15H61.7976V10.272C62.4576 9.612 62.9136 9.276 63.5856 9.276C64.4496 9.276 64.8216 9.792 64.8216 11.016V15H65.9136V10.872C65.9136 9.204 65.2896 8.316 63.9216 8.316C63.0336 8.316 62.3616 8.808 61.7616 9.408L61.7976 8.064V5.448H60.7056V15Z" fill="#FDFDFD"/>
<defs>
<linearGradient id="paint0_linear" x1="33.1333" y1="-1.92324e-06" x2="33.1333" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#EEEEEE" stop-opacity="0.1"/>
<stop offset="1" stop-opacity="0.1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/get-access-token.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/got-code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
assets/permissions-used.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
assets/private-folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
assets/register-app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

3772
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"private": true,
"name": "onedrive-cf-index",
"version": "1.0.0",
"description": "A template for kick starting a Cloudflare Workers project",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --write '**/*.{js,css,json,md}'",
"lint": "eslint src/**/* *.js --color --fix",
"preview": "wrangler preview --watch",
"dev": "wrangler dev"
},
"author": "spencerwooo <spencerwoo98@foxmail.com>",
"license": "MIT",
"devDependencies": {
"eslint": "^7.6.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"prettier": "^1.18.2"
},
"dependencies": {
"emoji-regex": "^9.2.0",
"font-awesome-filetypes": "^2.1.0",
"marked": "^2.0.1"
}
}

32
src/auth/config.js Normal file
View File

@ -0,0 +1,32 @@
/**
* Basic authentication.
* Enabled by default, you need to set PASSWORD secret using `wrangler secret put AUTH_PASSWORD`
*
* AUTH_ENABLED `false` to disable it
* NAME user name
* ENABLE_PATHS enable protection on specific folders/files
*/
export const AUTH_ENABLED = true
export const NAME = 'Lancer'
export const PASS = AUTH_PASSWORD
export const ENABLE_PATHS = ['/🌞 Personal']
/**
* RegExp for basic auth credentials
*
* credentials = auth-scheme 1*SP token68
* auth-scheme = "Basic" ; case insensitive
* token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
*/
export const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/
/**
* RegExp for basic auth user/pass
*
* user-pass = userid ":" password
* userid = *<TEXT excluding ":">
* password = *TEXT
*/
export const USER_PASS_REGEXP = /^([^:]*):(.*)$/

48
src/auth/credentials.js Normal file
View File

@ -0,0 +1,48 @@
import { CREDENTIALS_REGEXP, USER_PASS_REGEXP } from './config'
/**
* Object to represent user credentials.
*/
class Credentials {
constructor(name, pass) {
this.name = name
this.pass = pass
}
}
/**
* Parse basic auth to object.
*/
export function parseAuthHeader(string) {
if (typeof string !== 'string') {
return undefined
}
// parse header
const match = CREDENTIALS_REGEXP.exec(string)
if (!match) {
return undefined
}
// decode user pass
const userPass = USER_PASS_REGEXP.exec(atob(match[1]))
if (!userPass) {
return undefined
}
// return credentials object
return new Credentials(userPass[1], userPass[2])
}
export function unauthorizedResponse(body) {
return new Response(null, {
status: 401,
statusText: "'Authentication required.'",
body: body,
headers: {
'WWW-Authenticate': 'Basic realm="User Visible Realm"'
}
})
}

69
src/auth/onedrive.js Normal file
View File

@ -0,0 +1,69 @@
import config from '../config/default'
/**
* Get access token for microsoft graph API endpoints. Refresh token if needed.
*/
export async function getAccessToken() {
const timestamp = () => {
return Math.floor(Date.now() / 1000)
}
const refresh_token = await BUCKET.get('refresh_token')
// Fetch access token
const data = await BUCKET.get('onedrive', 'json')
if (data && data.access_token && timestamp() < data.expire_at) {
console.log('Fetched token from storage.')
return data.access_token
}
// Token expired, refresh access token with Microsoft API. Both international and china-specific API are supported
const oneDriveAuthEndpoint = `${config.apiEndpoint.auth}/token`
const resp = await fetch(oneDriveAuthEndpoint, {
method: 'POST',
body: `client_id=${config.client_id}&redirect_uri=${config.redirect_uri}&client_secret=${config.client_secret}
&refresh_token=${refresh_token}&grant_type=refresh_token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
if (resp.ok) {
console.info('Successfully refreshed access_token.')
const data = await resp.json()
// Update expiration time on token refresh
data.expire_at = timestamp() + data.expires_in
await BUCKET.put('onedrive', JSON.stringify(data))
console.info('Successfully updated access_token.')
// Finally, return access token
return data.access_token
} else {
// eslint-disable-next-line no-throw-literal
throw `getAccessToken error ${JSON.stringify(await resp.text())}`
}
}
/**
* Get & store siteID for finding sharepoint resources
*
* @param {string} accessToken token for accessing graph API
*/
export async function getSiteID(accessToken) {
let data = await BUCKET.get('sharepoint', 'json')
if (!data) {
const resp = await fetch(`${config.apiEndpoint.graph}${config.baseResource}?$select=id`, {
headers: {
Authorization: `bearer ${accessToken}`
}
})
if (resp.ok) {
data = await resp.json()
console.log('Got & stored site-id.')
await BUCKET.put('sharepoint', JSON.stringify({
id: data.id
}))
}
}
return data.id
}

109
src/config/default.js Normal file
View File

@ -0,0 +1,109 @@
/* eslint-disable no-irregular-whitespace */
const config = {
/**
* Configure the account/resource type for deployment (with 0 or 1)
* - accountType: controls account type, 0 for global, 1 for china (21Vianet)
* - driveType: controls drive resource type, 0 for onedrive, 1 for sharepoint document
*
* Followed keys is used for sharepoint resource, change them only if you gonna use sharepoint
* - hostName: sharepoint site hostname (e.g. 'name.sharepoint.com')
* - sitePath: sharepoint site path (e.g. '/sites/name')
* !Note: we do not support deploying onedrive & sharepoint at the same time
*/
type: {
accountType: 0,
driveType: 0,
hostName: null,
sitePath: null
},
client_id: '3d9dfeb5-aaa1-483a-a74b-8314ab5d7f56',
client_secret: CLIENT_SECRET,
/**
* Exactly the same `redirect_uri` in your Azure Application
*/
redirect_uri: 'https://localhost',
/**
* The base path for indexing, all files and subfolders are public by this tool. For example: `/Public`.
*/
base: '/Public',
/**
* Feature: Pagination when a folder has multiple(>${top}) files
* - top: specify the page size limit of the result set, a big `top` value will slow down the fetching speed
*/
pagination: {
enable: true,
top: 100 // default: 200, accepts a minimum value of 1 and a maximum value of 999 (inclusive)
},
/**
* Feature Caching
* Enable Cloudflare cache for path pattern listed below.
* Cache rules:
* - Entire File Cache 0 < file_size < entireFileCacheLimit
* - Chunked Cache entireFileCacheLimit <= file_size < chunkedCacheLimit
* - No Cache ( redirect to OneDrive Server ) others
*
* Difference between `Entire File Cache` and `Chunked Cache`
*
* `Entire File Cache` requires the entire file to be transferred to the Cloudflare server before
* the first byte sent to a client.
*
* `Chunked Cache` would stream the file content to the client while caching it.
* But there is no exact Content-Length in the response headers. ( Content-Length: chunked )
*
* `previewCache`: using CloudFlare cache to preview
*/
cache: {
enable: true,
entireFileCacheLimit: 10000000, // 10MB
chunkedCacheLimit: 100000000, // 100MB
previewCache: false,
paths: ['/🥟%20Some%20test%20files/Previews']
},
/**
* Feature: Thumbnail
* Show a thumbnail of image by ?thumbnail=small (small, medium, large)
* More details: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_thumbnails?view=odsp-graph-online#size-options
* Example: https://storage.spencerwoo.com/🥟%20Some%20test%20files/Previews/eb37c02438f.png?thumbnail=mediumSquare
* You can embed this link (url encoded) directly inside Markdown or HTML.
*/
thumbnail: true,
/**
* Small File Upload (<= 4MB)
* POST https://<base_url>/<directory_path>/?upload=<filename>&key=<secret_key>
* The <secret_key> is defined by you
*/
upload: {
enable: true,
key: 'Lancer'
},
/**
* Feature: Proxy Download
* Use Cloudflare as a relay to speed up download. (Especially in Mainland China)
* Example: https://storage.spencerwoo.com/🥟%20Some%20test%20files/Previews/eb37c02438f.png?raw&proxied
* You can also embed this link (url encoded) directly inside Markdown or HTML.
*/
proxyDownload: true
}
// IIFE to set apiEndpoint & baseResource
// eslint-disable-next-line no-unused-expressions
!(function({ accountType, driveType, hostName, sitePath }) {
config.apiEndpoint = {
graph: accountType ? 'https://microsoftgraph.chinacloudapi.cn/v1.0' : 'https://graph.microsoft.com/v1.0',
auth: accountType
? 'https://login.chinacloudapi.cn/common/oauth2/v2.0'
: 'https://login.microsoftonline.com/common/oauth2/v2.0'
}
config.baseResource = driveType ? `/sites/${hostName}:${sitePath}` : '/me/drive'
})(config.type)
export default config

244
src/fileView.js Normal file
View File

@ -0,0 +1,244 @@
import marked from 'marked'
import {
renderHTML
} from './render/htmlWrapper'
import {
renderPath
} from './render/pathUtil'
import {
renderMarkdown
} from './render/mdRenderer'
import {
preview,
extensions
} from './render/fileExtension'
/**
* Render code blocks with the help of marked and Markdown grammar
*
* @param {Object} file Object representing the code file to preview
* @param {string} lang The markdown code language string, usually just the file extension
*/
async function renderCodePreview(file, lang) {
const resp = await fetch(file['@microsoft.graph.downloadUrl'])
const content = await resp.text()
const toMarkdown = `\`\`\`${lang}\n${content}\n\`\`\``
const renderedCode = marked(toMarkdown)
return `<div class="markdown-body" style="margin-top: 0;">
${renderedCode}
</div>`
}
/**
* Render PDF with built-in PDF viewer
*
* @param {Object} file Object representing the PDF to preview
*/
function renderPDFPreview(file) {
return `<div id="pdf-preview-wrapper"></div>
<div class="loading-label">
<i class="fas fa-spinner fa-pulse"></i>
<span id="loading-progress">Loading PDF...</span>
</div>
<script>
// No variable declaration. Described in https://github.com/spencerwooo/onedrive-cf-index/pull/46
loadingLabel = document.querySelector('.loading-label')
loadingProgress = document.querySelector('#loading-progress')
function progress({ loaded, total }) {
loadingProgress.innerHTML = 'Loading PDF... ' + Math.round(loaded / total * 100) + '%'
}
fetch('${file['@microsoft.graph.downloadUrl']}').then(response => {
if (!response.ok) {
loadingLabel.innerHTML = '😟 ' + response.status + ' ' + response.statusText
throw Error(response.status + ' ' + response.statusText)
}
if (!response.body) {
loadingLabel.innerHTML = '😟 ReadableStream not yet supported in this browser. Please download the PDF directly using the button below.'
throw Error('ReadableStream not yet supported in this browser.')
}
const contentEncoding = response.headers.get('content-encoding')
const contentLength = response.headers.get(contentEncoding ? 'x-file-size' : 'content-length')
if (contentLength === null) {
loadingProgress.innerHTML = 'Loading progress unavailable. Please wait or download the PDF directly using the button below.'
console.error('Response size header unavailable')
return response
}
const total = parseInt(contentLength, 10)
let loaded = 0
return new Response(
new ReadableStream({
start(controller) {
const reader = response.body.getReader()
read()
function read() {
reader.read().then(({ done, value }) => {
if (done) {
controller.close()
return
}
loaded += value.byteLength
progress({ loaded, total })
controller.enqueue(value)
read()
}).catch(error => {
console.error(error)
controller.error(error)
})
}
}
})
)
})
.then(resp => resp.blob())
.then(blob => {
const pdfFile = new Blob([blob], { type: 'application/pdf' })
const pdfFileUrl = URL.createObjectURL(pdfFile)
loadingLabel.classList.add('fade-out-bck')
setTimeout(() => {
loadingLabel.remove()
document.querySelector('#pdf-preview-wrapper').classList.add('fade-in-fwd')
PDFObject.embed(pdfFileUrl, '#pdf-preview-wrapper', {
height: '80vh',
fallbackLink: '<p>😟 This browser does not support previewing PDF, please download the PDF directly using the button below.</p>'
})
}, 600)
})
</script>`
}
/**
* Render image (jpg, png or gif)
*
* @param {Object} file Object representing the image to preview
*/
function renderImage(file) {
return `<div class="image-wrapper">
<img data-zoomable src="${file['@microsoft.graph.downloadUrl']}" alt="${file.name}" style="width: 100%; height: auto; position: relative;"></img>
</div>`
}
/**
* Render video (mp4, flv, m3u8, webm ...)
*
* @param {Object} file Object representing the video to preview
* @param {string} fileExt The file extension parsed
*/
function renderVideoPlayer(file, fileExt) {
return `<div id="dplayer"></div>
<script>
dp = new DPlayer({
container: document.getElementById('dplayer'),
theme: '#0070f3',
video: {
url: '${file['@microsoft.graph.downloadUrl']}',
type: '${fileExt}'
}
})
</script>`
}
/**
* Render audio (mp3, aac, wav, oga ...)
*
* @param {Object} file Object representing the audio to preview
*/
function renderAudioPlayer(file) {
return `<div id="aplayer"></div>
<script>
ap = new APlayer({
container: document.getElementById('aplayer'),
theme: '#0070f3',
audio: [{
name: '${file.name}',
url: '${file['@microsoft.graph.downloadUrl']}'
}]
})
</script>`
}
/**
* File preview fallback
*
* @param {string} fileExt The file extension parsed
*/
function renderUnsupportedView(fileExt) {
return `<div class="markdown-body" style="margin-top: 0;">
<p>Sorry, we don't support previewing <code>.${fileExt}</code> files as of today. You can download the file directly.</p>
</div>`
}
/**
* Render preview of supported file format
*
* @param {Object} file Object representing the file to preview
* @param {string} fileExt The file extension parsed
*/
async function renderPreview(file, fileExt, cacheUrl) {
if (cacheUrl) {
// This will change your download url too! (proxied download)
file['@microsoft.graph.downloadUrl'] = cacheUrl
}
switch (extensions[fileExt]) {
case preview.markdown:
return await renderMarkdown(file['@microsoft.graph.downloadUrl'], '', 'style="margin-top: 0;"')
case preview.text:
return await renderCodePreview(file, '')
case preview.image:
return renderImage(file)
case preview.code:
return await renderCodePreview(file, fileExt)
case preview.pdf:
return renderPDFPreview(file)
case preview.video:
return renderVideoPlayer(file, fileExt)
case preview.audio:
return renderAudioPlayer(file)
default:
return renderUnsupportedView(fileExt)
}
}
export async function renderFilePreview(file, path, fileExt, cacheUrl) {
const el = (tag, attrs, content) => `<${tag} ${attrs.join(' ')}>${content}</${tag}>`
const div = (className, content) => el('div', [`class=${className}`], content)
const body = div(
'container',
div('path', renderPath(path) + ` / ${file.name}`) +
div('items', el('div', ['style="padding: 1rem 1rem;"'], await renderPreview(file, fileExt, cacheUrl))) +
div(
'download-button-container',
el(
'a',
['class="download-button"', `href="${file['@microsoft.graph.downloadUrl']}"`, 'data-turbolinks="false"'],
'<i class="far fa-arrow-alt-circle-down"></i> 立即下载'
) + el(
'a',
['class="download-button proxy"','', 'data-turbolinks="false"'],
'<i class="far fa-arrow-alt-circle-down"></i> 复制代理下载地址'
)+el(
'a',
['class="download-button long"','', 'data-turbolinks="false"'],
'<i class="far fa-arrow-alt-circle-down"></i> 复制下载地址'
)
)
)
return renderHTML(body)
}

156
src/files/load.js Normal file
View File

@ -0,0 +1,156 @@
import config from '../config/default'
import { getAccessToken } from '../auth/onedrive'
/**
* Cloudflare cache instance
*/
const cache = caches.default
/**
* Cache downloadUrl according to caching rules.
* @param {Request} request client's request
* @param {integer} fileSize
* @param {string} downloadUrl
* @param {function} fallback handle function if the rules is not satisfied
*/
async function setCache(request, fileSize, downloadUrl, fallback) {
if (fileSize < config.cache.entireFileCacheLimit) {
console.info(`Cache entire file ${request.url}`)
const remoteResp = await fetch(downloadUrl)
const resp = new Response(remoteResp.body, {
headers: {
'Content-Type': remoteResp.headers.get('Content-Type'),
ETag: remoteResp.headers.get('ETag')
},
status: remoteResp.status,
statusText: remoteResp.statusText
})
await cache.put(request, resp.clone())
return resp
} else if (fileSize < config.cache.chunkedCacheLimit) {
console.info(`Chunk cache file ${request.url}`)
const remoteResp = await fetch(downloadUrl)
const { readable, writable } = new TransformStream()
remoteResp.body.pipeTo(writable)
const resp = new Response(readable, {
headers: {
'Content-Type': remoteResp.headers.get('Content-Type'),
ETag: remoteResp.headers.get('ETag')
},
status: remoteResp.status,
statusText: remoteResp.statusText
})
await cache.put(request, resp.clone())
return resp
} else {
console.info(`No cache ${request.url} because file_size(${fileSize}) > limit(${config.cache.chunkedCacheLimit})`)
return await fallback(downloadUrl)
}
}
/**
* Redirect to the download url.
* @param {string} downloadUrl
*/
async function directDownload(downloadUrl) {
console.info(`DirectDownload -> ${downloadUrl}`)
return new Response(null, {
status: 302,
headers: {
Location: downloadUrl.slice(6)
}
})
}
/**
* Download a file using Cloudflare as a relay.
* @param {string} downloadUrl
*/
async function proxiedDownload(downloadUrl) {
console.info(`ProxyDownload -> ${downloadUrl}`)
const remoteResp = await fetch(downloadUrl)
const { readable, writable } = new TransformStream()
remoteResp.body.pipeTo(writable)
return new Response(readable, remoteResp)
}
export async function handleFile(request, pathname, downloadUrl, { proxied = false, fileSize = 0 }) {
if (config.cache && config.cache.enable && config.cache.paths.filter(p => pathname.startsWith(p)).length > 0) {
return setCache(request, fileSize, downloadUrl, proxied ? proxiedDownload : directDownload)
}
return (proxied ? proxiedDownload : directDownload)(downloadUrl)
}
export async function handleUpload(request, pathname, filename) {
const url = `${config.apiEndpoint.graph}/me/drive/root:${encodeURI(config.base) +
(pathname.slice(-1) === '/' ? pathname : pathname + '/')}${filename}:/content`
return await fetch(url, {
method: 'PUT',
headers: {
Authorization: `bearer ${await getAccessToken()}`,
...request.headers
},
body: request.body
})
}
export async function HttpUpload(ext) {
let tempName="temp"+(Math.random()+"").replace(".",'')+'.'+ext;
var date = new Date();
var path=`${date .getFullYear()}-${date .getMonth()+1}-${date .getDate()}/${new Date().getHours()+8}/`
const url = `${config.apiEndpoint.graph}/me/drive/root:/${encodeURI(config.base+"/游客上传/")+path+tempName}:/createUploadSession`
let rs= await fetch(url, {
method: 'POST',
headers: {
Authorization: `bearer ${await getAccessToken()}`,
},
})
rs=JSON.parse(await gatherResponse(rs));
rs.path=encodeURI("/游客上传/")+path+tempName;
return rs;
}
export async function gatherResponse(response) {
const { headers } = response
const contentType = headers.get("content-type") || ""
if (contentType.includes("application/json")) {
return JSON.stringify(await response.json())
}
else if (contentType.includes("application/text")) {
return await response.text()
}
else if (contentType.includes("text/html")) {
return await response.text()
}
else {
return await response.text()
}
}
export async function readRequestBody(request) {
const { headers } = request
const contentType = headers.get("content-type") || ""
if (contentType.includes("application/json")) {
return JSON.stringify(await request.json())
}
else if (contentType.includes("application/text")) {
return await request.text()
}
else if (contentType.includes("text/html")) {
return await request.text()
}
else if (contentType.includes("form")) {
const formData = await request.formData()
const body = {}
for (const entry of formData.entries()) {
body[entry[0]] = entry[1]
}
return JSON.stringify(body)
}
else {
const myBlob = await request.blob()
const objectURL = URL.createObjectURL(myBlob)
return objectURL
}
}

119
src/folderView.js Normal file
View File

@ -0,0 +1,119 @@
import emojiRegex from 'emoji-regex/RGI_Emoji'
import { getClassNameForMimeType, getClassNameForFilename } from 'font-awesome-filetypes'
import { renderHTML } from './render/htmlWrapper'
import { renderPath } from './render/pathUtil'
import { renderMarkdown } from './render/mdRenderer'
/**
* Convert bytes to human readable file size
*
* @param {Number} bytes File size in bytes
* @param {Boolean} si 1000 - true; 1024 - false
*/
function readableFileSize(bytes, si) {
bytes = parseInt(bytes, 10)
var thresh = si ? 1000 : 1024
if (Math.abs(bytes) < thresh) {
return bytes + ' B'
}
var units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
var u = -1
do {
bytes /= thresh
++u
} while (Math.abs(bytes) >= thresh && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
}
/**
* Render Folder Index
*
* @param {*} items
* @param {*} isIndex don't show ".." on index page.
*/
export async function renderFolderView(items, path, request) {
const isIndex = path === '/'
const el = (tag, attrs, content) => `<${tag} ${attrs.join(' ')}>${content}</${tag}>`
const div = (className, content) => el('div', [`class=${className}`], content)
const item = (icon, fileName, fileAbsoluteUrl, size, emojiIcon) =>
el(
'a',
[`href="${fileAbsoluteUrl}"`, 'class="item"', size ? `size="${size}"` : ''],
(emojiIcon ? el('i', ['style="font-style: normal"'], emojiIcon) : el('i', [`class="${icon}"`], '')) +
fileName +
el('div', ['style="flex-grow: 1;"'], '') +
(fileName === '..' ? '' : el('span', ['class="size"'], readableFileSize(size)))
)
const intro = `<footer style="padding-bottom:1rem;">白嫖的OneDrive</footer>`
// Check if current directory contains README.md, if true, then render spinner
let readmeExists = false
let readmeFetchUrl = ''
const body = div(
'container',
div('path', renderPath(path)) +
div(
'items',
el(
'div',
['style="min-width: 600px"'],
(!isIndex ? item('far fa-folder', '..', `${path}..`) : '') +
items
.map(i => {
// Check if the current item is a folder or a file
if ('folder' in i) {
const emoji = emojiRegex().exec(i.name)
if (emoji && !emoji.index) {
return item('', i.name.replace(emoji, '').trim(), `${path}${i.name}/`, i.size, emoji[0])
} else {
return item('far fa-folder', i.name, `${path}${i.name}/`, i.size)
}
} else if ('file' in i) {
// Check if README.md exists
if (!readmeExists) {
// TODO: debugging for README preview rendering
console.log("渲染readme",i)
readmeExists = i.name.toLowerCase() === 'readme.md'
readmeFetchUrl = i['@microsoft.graph.downloadUrl']
console.log("readmeURl",readmeFetchUrl)
}
// Render file icons
let fileIcon = getClassNameForMimeType(i.file.mimeType)
if (fileIcon === 'fa-file') {
// Check for files that haven't been rendered as expected
const extension = i.name.split('.').pop()
if (extension === 'md') {
fileIcon = 'fab fa-markdown'
} else if (['7z', 'rar', 'bz2', 'xz', 'tar', 'wim'].includes(extension)) {
fileIcon = 'far fa-file-archive'
} else if (['flac', 'oga', 'opus'].includes(extension)) {
fileIcon = 'far fa-file-audio'
} else {
fileIcon = `far ${getClassNameForFilename(i.name)}`
}
} else {
fileIcon = `far ${fileIcon}`
}
return item(fileIcon, i.name, `${path}${i.name}`, i.size)
} else {
console.log(`unknown item type ${i}`)
}
})
.join('')
)
) +
(readmeExists? await renderMarkdown(readmeFetchUrl, 'fade-in-fwd', '') : '') +
(isIndex ? intro : '')
)
console.log("是否显示",readmeExists,isIndex,readmeExists && !isIndex)
return renderHTML(body, ...[request.pLink, request.pIdx])
}

267
src/index.js Normal file
View File

@ -0,0 +1,267 @@
import config from './config/default'
import {
AUTH_ENABLED,
NAME,
ENABLE_PATHS
} from './auth/config'
import {
parseAuthHeader,
unauthorizedResponse
} from './auth/credentials'
import {
getAccessToken,
getSiteID
} from './auth/onedrive'
import {
handleFile,
handleUpload,
HttpUpload,
readRequestBody
} from './files/load'
import {
extensions
} from './render/fileExtension'
import {
renderFolderView
} from './folderView'
import {
renderFilePreview
} from './fileView'
import {
renderUpload
} from './render/htmlWrapper'
addEventListener('fetch', event => {
event.respondWith(handle(event.request))
})
async function handle(request) {
if (AUTH_ENABLED === false) {
return handleRequest(request)
}
if (AUTH_ENABLED === true) {
const pathname = decodeURIComponent(new URL(request.url).pathname).toLowerCase()
const privatePaths = ENABLE_PATHS.map(i => i.toLowerCase())
if (privatePaths.filter(p => pathname.toLowerCase().startsWith(p)).length > 0 || /__Lock__/gi.test(pathname)) {
const credentials = parseAuthHeader(request.headers.get('Authorization'))
if (!credentials || credentials.name !== NAME || credentials.pass !== AUTH_PASSWORD) {
return unauthorizedResponse('Unauthorized')
}
return handleRequest(request)
} else {
return handleRequest(request)
}
} else {
console.info('Auth error unexpected.')
}
}
// Cloudflare cache instance
const cache = caches.default
const base = encodeURI(config.base).replace(/\/$/, '')
/**
* Format and regularize directory path for OneDrive API
*
* @param {string} pathname The absolute path to file
* @param {boolean} isRequestFolder is indexing folder or not
*/
function wrapPathName(pathname, isRequestFolder) {
pathname = base + pathname
const isIndexingRoot = pathname === '/'
if (isRequestFolder) {
if (isIndexingRoot) return ''
return `:${pathname.replace(/\/$/, '')}:`
}
return `:${pathname}`
}
async function handleRequest(request) {
//判断改请求是否以缓存
if (config.cache && config.cache.enable) {
const maybeResponse = await cache.match(request)
if (maybeResponse) return maybeResponse
}
const accessToken = await getAccessToken()
if (config.type.driveType) {
config.baseResource = `/sites/${await getSiteID(accessToken)}/drive`
}
const {
pathname,
searchParams
} = new URL(request.url)
const neoPathname = pathname.replace(/pagination$/, '')
const isRequestFolder = pathname.endsWith('/') || searchParams.get('page')
const rawFile = searchParams.get('raw') !== null
const thumbnail = config.thumbnail ? searchParams.get('thumbnail') : false
const proxied = config.proxyDownload ? searchParams.get('proxied') !== null : false
if (thumbnail) {
const url = `${config.apiEndpoint.graph}${config.baseResource}/root${wrapPathName(
neoPathname,
isRequestFolder
)}:/thumbnails/0/${thumbnail}/content`
const resp = await fetch(url, {
headers: {
Authorization: `bearer ${accessToken}`
}
})
return await handleFile(request, pathname, resp.url, {
proxied
})
}
// 游客上传目录
if (pathname == "/upload" && request.method == "GET") {
return new Response(renderUpload(), {
headers: {
'Access-Control-Allow-Origin': '*',
'content-type': 'text/html'
}
})
}
if (pathname == "/upload" && request.method == "POST") {
let postParms = JSON.parse(await readRequestBody(request));
let rs = await HttpUpload(postParms.ext);
return new Response(JSON.stringify(rs), {
'Access-Control-Allow-Origin': '*',
'content-type': 'application/json'
})
}
let url = `${config.apiEndpoint.graph}${config.baseResource}/root${wrapPathName(neoPathname, isRequestFolder)}${
isRequestFolder
? '/children' + (config.pagination.enable && config.pagination.top ? `?$top=${config.pagination.top}` : '')
: '?select=%40microsoft.graph.downloadUrl,name,size,file'
}`
// get & set {pLink ,pIdx} for fetching and paging
const paginationLink = request.headers.get('pLink')
const paginationIdx = request.headers.get('pIdx') - 0
if (paginationLink && paginationLink !== 'undefined') {
url += `&$skiptoken=${paginationLink}`
}
const resp = await fetch(url, {
headers: {
Authorization: `bearer ${accessToken}`
}
})
let error = null
if (resp.ok) {
const data = await resp.json()
if (data['@odata.nextLink']) {
request.pIdx = paginationIdx || 1
request.pLink = data['@odata.nextLink'].match(/&\$skiptoken=(.+)/)[1]
} else if (paginationIdx) {
request.pIdx = -paginationIdx
}
if ('file' in data) {
// Render file preview view or download file directly
const fileExt = data.name
.split('.')
.pop()
.toLowerCase()
// Render file directly if url params 'raw' are given
if (rawFile || !(fileExt in extensions)) {
return await handleFile(request, pathname, data['@microsoft.graph.downloadUrl'], {
proxied,
fileSize: data.size
})
}
//xhuan 修改的预览逻辑
if (searchParams.get("preview") != null) {
let type = searchParams.get("preview");
if (type == "" || type == "cf") {
let {
readable,
writable
} = new TransformStream()
let previewData = await fetch(data['@microsoft.graph.downloadUrl'])
previewData.body.pipeTo(writable)
console.log(JSON.stringify(previewData.headers))
return new Response(readable, {
headers: {
'cache-control': 'max-age=2592000',
'content-length': data['size'],
'content-type': data['file']['mimeType'],
'xhuan': "hello"
}
})
}else{
return new Response('你不对劲',{status:301,headers:{'Location':data['@microsoft.graph.downloadUrl']}})
}
}
// Add preview by CloudFlare worker cache feature
let cacheUrl = null
if (config.cache.enable && config.cache.previewCache && data.size < config.cache.chunkedCacheLimit) {
cacheUrl = request.url + '?proxied&raw'
}
return new Response(await renderFilePreview(data, pathname, fileExt, cacheUrl || null), {
headers: {
'Access-Control-Allow-Origin': '*',
'content-type': 'text/html'
}
})
} else {
// Render folder view, list all children files
if (config.upload && request.method === 'POST') {
const filename = searchParams.get('upload')
const key = searchParams.get('key')
if (filename && key && config.upload.key === key) {
return await handleUpload(request, neoPathname, filename)
} else {
return new Response('', {
status: 400
})
}
}
// 302 all folder requests that doesn't end with /
if (!isRequestFolder) {
return Response.redirect(request.url + '/', 302)
}
return new Response(await renderFolderView(data.value, neoPathname, request), {
headers: {
'Access-Control-Allow-Origin': '*',
'content-type': 'text/html'
}
})
}
} else {
error = (await resp.json()).error
}
if (error) {
const body = JSON.stringify(error)
switch (error.code) {
case 'itemNotFound':
return new Response(body, {
status: 404,
headers: {
'content-type': 'application/json'
}
})
default:
return new Response(body, {
status: 500,
headers: {
'content-type': 'application/json'
}
})
}
}
}

2
src/render/favicon.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,56 @@
const preview = {
markdown: 'markdown',
image: 'image',
text: 'text',
pdf: 'pdf',
code: 'code',
video: 'video',
audio: 'audio'
}
const extensions = {
gif: preview.image,
jpeg: preview.image,
jpg: preview.image,
png: preview.image,
md: preview.markdown,
markdown: preview.markdown,
mdown: preview.markdown,
pdf: preview.pdf,
c: preview.code,
cpp: preview.code,
js: preview.code,
java: preview.code,
sh: preview.code,
cs: preview.code,
py: preview.code,
css: preview.code,
html: preview.code,
ts: preview.code,
vue: preview.code,
json: preview.code,
yaml: preview.code,
toml: preview.code,
txt: preview.text,
mp4: preview.video,
flv: preview.video,
webm: preview.video,
m3u8: preview.video,
mkv: preview.video,
mp3: preview.audio,
m4a: preview.audio,
aac: preview.audio,
wav: preview.audio,
ogg: preview.audio,
oga: preview.audio,
opus: preview.audio,
flac: preview.audio
}
export { extensions, preview }

319
src/render/htmlWrapper.js Normal file
View File

@ -0,0 +1,319 @@
import {
favicon
} from './favicon'
const COMMIT_HASH = 'ad7b598'
const pagination = (pIdx, attrs) => {
const getAttrs = (c, h, isNext) =>
`class="${c}" ${h ? `href="pagination?page=${h}"` : ''} ${isNext === undefined ? '' : `id=${c.includes('pre') ? 'pagination-pre' : 'pagination-next'}`
}`
if (pIdx) {
switch (pIdx) {
case pIdx < 0 ? pIdx:
null:
attrs = [getAttrs('pre', -pIdx - 1, 0), getAttrs('next off', null)]
break
case 1:
attrs = [getAttrs('pre off', null), getAttrs('next', pIdx + 1, 1)]
break
default:
attrs = [getAttrs('pre', pIdx - 1, 0), getAttrs('next', pIdx + 1, 1)]
}
return `${`<a ${attrs[0]}><i class="fas fa-angle-left" style="font-size: 8px;"></i> PREV</a>`}<span>Page ${pIdx}</span> ${`<a ${attrs[1]}>NEXT <i class="fas fa-angle-right" style="font-size: 8px;"></i></a>`}`
}
return ''
}
export function renderHTML(body, pLink, pIdx) {
pLink = pLink || ''
const p = 'window[pLinkId]'
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge, chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>pscgyLancer's OneDrive</title>
<link rel="shortcut icon" type="image/png" sizes="16x16" href="${favicon}" />
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.13.1/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/gh/spencerwooo/onedrive-cf-index@${COMMIT_HASH}/themes/spencer.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/gh/sindresorhus/github-markdown-css@gh-pages/github-markdown.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/gh/spencerwooo/onedrive-cf-index@${COMMIT_HASH}/themes/prism-github.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.17.1/prism.min.js" data-manual></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.17.1/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.0.6/dist/medium-zoom.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/turbolinks@5.2.0/dist/turbolinks.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/pipwerks/PDFObject/pdfobject.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flv.js@1.5.0/dist/flv.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dplayer@1.26.0/dist/DPlayer.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script>
<style>
.markdown-body {
background: #ccc;
padding: 1rem;
border-radius: 17px;
font-family: var(--base-font-family) !important;
margin-top: 1.5rem;
}
.download-button-container{
display: flex;
}
</style>
</head>
<body>
<nav id="navbar" data-turbolinks-permanent><div class="brand">pscgyLancer's OneDrive</div></nav>
${body}
<div class="paginate-container">${pagination(pIdx)}</div>
<div id="flex-container" data-turbolinks-permanent style="flex-grow: 1;"></div>
<script>
if (typeof ap !== "undefined" && ap.paused !== true) {
ap.destroy()
ap = undefined
}
if (typeof dp !== "undefined" && dp.paused !== true) {
dp.destroy()
dp = undefined
}
Prism.highlightAll()
mediumZoom('[data-zoomable]')
Turbolinks.Location.prototype.isHTML = () => {return true}
Turbolinks.start()
pagination()
clipboard()
function clipboard(){
var clipboard = new ClipboardJS('.proxy',{
text: function() {
return window.location.href+"?preview";
}
});
clipboard.on('success', function(e) {
alert('复制成功!')
});
clipboard.on('error', function(e) {
alert('复制失败!'+e)
});
var clipboard2 = new ClipboardJS('.long',{
text: function() {
return window.location.href+"?preview=301";
}
});
clipboard2.on('success', function(e) {
alert('复制成功!')
});
clipboard2.on('error', function(e) {
alert('复制失败!'+e)
});
}
function pagination() {
if ('${pLink ? 1 : ''}') {
if (location.pathname.endsWith('/')) {
pLinkId = history.state.turbolinks.restorationIdentifier
${p} = { link: ['${pLink}'], idx: 1 }
} else if (!window.pLinkId) {
history.pushState(history.state, '', location.pathname.replace('pagination', '/'))
return
}
if (${p}.link.length < ${p}.idx) (${p} = { link: [...${p}.link, '${pLink}'], idx: ${p}.idx })
}
listen = ({ isNext }) => {
isNext ? ${p}.idx++ : ${p}.idx--
addEventListener(
'turbolinks:request-start',
event => {
const xhr = event.data.xhr
xhr.setRequestHeader('pLink', ${p}.link[${p}.idx -2])
xhr.setRequestHeader('pIdx', ${p}.idx + '')
},
{ once: true }
)
}
preBtn = document.getElementById('pagination-pre')
nextBtn = document.getElementById('pagination-next')
if (nextBtn) {
nextBtn.addEventListener('click', () => listen({ isNext: true }), { once: true })
}
if (preBtn) {
preBtn.addEventListener('click', () => listen({ isNext: false }), { once: true })
}
}
</script>
</body>
</html>`
}
export function renderUpload() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge, chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>pscgyLancer's OneDrive</title>
<link rel="shortcut icon" type="image/png" sizes="16x16" href="${favicon}" />
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.13.1/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/gh/spencerwooo/onedrive-cf-index@ad7b598/themes/spencer.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/gh/fex-team/webuploader@0.1.5/examples/image-upload/jquery.js"></script>
<script src="https://cdn.jsdelivr.net/gh/fex-team/webuploader@0.1.5/dist/webuploader.html5only.js"></script>
<style>
.upload-title {
display: flex;
}
.btn-upload {
padding: 10px 20px;
background: rgb(0 183 238);
border: unset;
border-radius: 8px;
text-align: center;
font-size: 15px;
color: #fff;
}
.upload-title {
padding: 20px 1rem;
}
.upload-files {
display: flex;
flex-wrap: wrap;
}
#picker input {
display: none;
}
.upload-container {
height: 30rem;
border: 2px dashed #ccc;
}
.upload-files .file {
margin-top: 13px;
align-items: flex-end;
direction: 2;
margin-left: 12px;
height: 8rem;
width: 7rem;
border: 1px solid darkturquoise;
}
.upload-files .file .img {
width: 100%;
height: 6.6rem;
text-align: center;
line-height: 6.6rem;
background: #ccc;
}
</style>
</head>
<body>
<nav id="navbar" data-turbolinks-permanent>
<div class="brand">pscgyLancer's OneDrive</div>
</nav>
<div class="container">
<div class="items">
<div style="min-width: 600px;padding: 1rem;">
<div class="upload-container">
<div class="upload-title">
<div class="btn-upload" id="picker">选择文件</div>
</div>
<div class="upload-files">
</div>
</div>
</div>
</div>
</div>
<div id="flex-container" data-turbolinks-permanent style="flex-grow: 1;"></div>
<script>
$(function () {
$fileServer =""
var uploader = new WebUploader.Uploader({
chunked:true,
chunkSize:5242880,
chunkRetry:5,
server: $fileServer,
resize: true,
pick: '#picker',
dnd: '.upload-container',
paste:'.upload-container',
method: 'PUT',
sendAsBinary: true,
threads: 1,
});
$files = $('.upload-files');
uploader.on('beforeFileQueued', function (file) {
var md5 = uploader.md5File(file);
});
uploader.on('uploadBeforeSend', function (object, data, headers) {
headers['Content-Range']="bytes "+(object.start.toString())+"-"+((object.end-1).toString())+"/"+(object.total)
});
uploader.on('uploadAccept', function (object,rel) {
});
var fils=$('.upload-files')
uploader.on('fileQueued', function (file) {
var md5 = uploader.md5File(file);
let self=this;
$.post('/upload', { ext: file.source.ext }, function (res) {
var json = JSON.parse(res);
$fileServer= json.uploadUrl;
self.options.server=$fileServer;
file.webPath=json.path;
$.get($fileServer, function (res) {
uploader.upload(file);
}, 'json')
})
var temp='<div class="file" id="'+file.id+'"><div class="img" >0%</div><div class="file-name">'+file.name+'</div></div>'
fils.append(temp)
});
uploader.on('uploadProgress', function (file, percentage) {
fils.find('#'+file.id).find('.img').text(percentage * 100 + '%')
});
uploader.on('uploadSuccess', function (file) {
var fileName= fils.find('#'+file.id).find('.file-name').text()
fils.find('#'+file.id).find('.file-name').empty()
fils.find('#'+file.id).find('.file-name').append('<a href="'+file.webPath+'" target="_blank">'+fileName+'</a>')
console.log(file.webPath);
$('#' + file.id).find('p.state').text('已上传');
});
uploader.on('uploadError', function (file) {
$('#' + file.id).find('p.state').text('上传出错');
});
uploader.on('uploadComplete', function (file) {
$('#' + file.id).find('.progress').fadeOut();
});
})
</script>
</body>
</html>`;
}

50
src/render/mdRenderer.js Normal file
View File

@ -0,0 +1,50 @@
import marked from 'marked'
import { cleanUrl } from 'marked/src/helpers'
// Rewrite renderer, see original at: https://github.com/markedjs/marked/blob/master/src/Renderer.js
const renderer = new marked.Renderer()
renderer.image = (href, title, text) => {
href = cleanUrl(false, null, href)
if (href === null) {
return text
}
let url
try {
// Check if href is relative
url = new URL(href).href
} catch (TypeError) {
// Add param raw
if (href.includes('?')) {
const urlSplitParams = href.split('?')
const param = new URLSearchParams(urlSplitParams[1])
param.append('raw')
url = urlSplitParams[0] + '?' + param.toString()
} else {
url = href + '?raw'
}
}
let out = '<img data-zoomable src="' + url + '" alt="' + text + '"'
if (title) {
out += ' title="' + title + '"'
}
return (out += '>')
}
marked.setOptions({ renderer: renderer })
/**
* Fetch and render Markdown files
*
* @param {string} mdDirectLink Markdown direct download link
* @param {string} classAttr Class attribute for animations (like fade-in-fwd)
* @param {string} style CSS inline styles for Markdown block
*/
export async function renderMarkdown(mdDirectLink, classAttr, style) {
console.log("进来了")
const resp = await fetch(mdDirectLink)
const content = await resp.text()
const renderedMd = marked(content)
return `<div class="markdown-body ${classAttr}" ${style}>
${renderedMd}
</div>`
}

35
src/render/pathUtil.js Normal file
View File

@ -0,0 +1,35 @@
/**
* Return absolute path of current working directory
*
* @param {array} pathItems List of current directory items
* @param {number} idx Current depth inside home
*/
function getPathLink(pathItems, idx) {
const pathList = pathItems.slice(0, idx + 1)
if (pathList.length === 1) {
return '/'
}
pathList[0] = ''
return pathList.join('/') + '/'
}
/**
* Render directory breadcrumb
*
* @param {string} path current working directory, for instance: /🥑 Course PPT for CS (BIT)/2018 - - /
*/
export function renderPath(path) {
const pathItems = path.split('/')
pathItems[0] = '/'
pathItems.pop()
const link = (href, content) => `<a href="${href}">${content}</a>`
const breadcrumb = []
pathItems.forEach((item, idx) => {
breadcrumb.push(link(getPathLink(pathItems, idx), idx === 0 ? '🚩 Home' : decodeURIComponent(item)))
})
return breadcrumb.join(' / ')
}

87
themes/prism-github.css Normal file
View File

@ -0,0 +1,87 @@
code,
code[class*='language-'],
pre[class*='language-'] {
color: #24292e;
text-align: left;
white-space: pre;
word-spacing: normal;
tab-size: 4;
hyphens: none;
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
line-height: 1.4;
direction: ltr;
cursor: text;
}
pre[class*='language-'] {
overflow: auto;
margin: 1em 0;
padding: 1.2em;
border-radius: 3px;
font-size: 85%;
}
p code,
li code,
table code {
margin: 0;
border-radius: 3px;
padding: 0.2em 0;
font-size: 85%;
}
p code:before,
p code:after,
li code:before,
li code:after,
table code:before,
table code:after {
letter-spacing: -0.2em;
content: '\00a0';
}
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6a737d;
}
.token.punctuation,
.token.string,
.token.atrule,
.token.attr-value {
color: #032f62;
}
.token.property,
.token.tag {
color: #22863a;
}
.token.boolean,
.token.number {
color: #005cc5;
}
.token.selector,
.token.attr-name,
.token.attr-value .punctuation:first-child,
.token.keyword,
.token.regex,
.token.important {
color: #d73a49;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string {
color: #005cc5;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: 0.7;
}

320
themes/spencer.css Normal file
View File

@ -0,0 +1,320 @@
@import url('https://fonts.googleapis.cnpmjs.org/css2?family=Inter:wght@400;700&display=swap');
:root {
--base-font-family: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, pingfang sc,
source han sans sc, noto sans cjk sc, sarasa gothic sc, microsoft yahei, sans-serif, Apple Color Emoji,
Segoe UI Emoji;
}
html,
body {
margin: 0;
padding: 0;
}
body {
font-family: var(--base-font-family);
-webkit-font-smoothing: antialiased;
background: #fafafa;
display: flex;
align-items: center;
justify-content: space-between;
flex-flow: column nowrap;
height: 100vh;
}
nav {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 2rem;
padding: 0.8rem 0rem;
background: #ffffff;
color: #000000;
font-size: 1.4rem;
box-shadow: inset 0 -1px #eaeaea;
margin-bottom: 1.5rem;
}
a {
transition: 0.2s all ease-in-out;
color: #0070f3;
}
.brand {
font-weight: bold;
}
.container,
.paginate-container {
width: calc(100% - 40px);
max-width: 800px;
margin: 0 auto;
}
.path {
margin-bottom: 1.5rem;
font-size: 0.88rem;
color: #999;
}
.path a {
text-decoration: none;
color: #333;
}
.items {
overflow-x: auto;
display: flex;
flex-flow: column nowrap;
background: white;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
border-radius: 8px;
}
a.item {
display: flex;
align-items: center;
text-decoration: none;
color: #000000;
padding: 0.8rem 1rem;
transition: 0.2s all ease-in-out;
}
a.item i {
margin-right: 0.5rem;
}
a.item .size {
opacity: 0.6;
}
footer {
font-size: 0.8rem;
color: #666;
width: calc(100% - 40px);
padding: 1.6rem 0rem;
text-align: center;
}
footer a {
text-decoration: none;
}
a:hover,
a.item:hover {
opacity: 0.6;
}
.download-button-container {
width: 100%;
text-align: center;
box-sizing: border-box;
}
.download-button {
display: block;
background-color: #000;
color: #ffffff;
cursor: pointer;
font-weight: bold;
text-decoration: none;
padding: 0.5rem 1rem;
margin: 1.5rem auto;
max-width: 180px;
user-select: none;
border-radius: 2px;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
}
.markdown-body {
background: #ccc;
padding: 1rem;
border-radius: 17px;
font-family: var(--base-font-family) !important;
margin-top: 1.5rem;
}
.loading-label {
font-family: var(--base-font-family) !important;
text-align: center;
margin: 3rem 0rem;
opacity: 0.6;
}
.paginate-container {
margin-top: 1.5rem;
display: grid;
justify-content: space-between;
grid-template-columns: repeat(3, auto);
}
.paginate-container a {
cursor: pointer;
text-decoration: none;
}
.paginate-container a.off {
pointer-events: none;
opacity: 0.5;
}
.fade-in-bottom {
-webkit-animation: fade-in-bottom 0.3s cubic-bezier(0.39, 0.575, 0.565, 1) both;
animation: fade-in-bottom 0.3s cubic-bezier(0.39, 0.575, 0.565, 1) both;
}
.fade-in-fwd {
-webkit-animation: fade-in-fwd 0.6s cubic-bezier(0.39, 0.575, 0.565, 1) both;
animation: fade-in-fwd 0.6s cubic-bezier(0.39, 0.575, 0.565, 1) both;
}
.fade-out-bck {
-webkit-animation: fade-out-bck 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
animation: fade-out-bck 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@-webkit-keyframes fade-in-bottom {
0% {
-webkit-transform: translateY(50px);
transform: translateY(50px);
opacity: 0;
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
}
@keyframes fade-in-bottom {
0% {
-webkit-transform: translateY(50px);
transform: translateY(50px);
opacity: 0;
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
}
@-webkit-keyframes fade-in-fwd {
0% {
-webkit-transform: translateZ(-80px);
transform: translateZ(-80px);
opacity: 0;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}
@keyframes fade-in-fwd {
0% {
-webkit-transform: translateZ(-80px);
transform: translateZ(-80px);
opacity: 0;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}
@-webkit-keyframes fade-out-bck {
0% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
100% {
-webkit-transform: translateZ(-80px);
transform: translateZ(-80px);
opacity: 0;
}
}
@keyframes fade-out-bck {
0% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
100% {
-webkit-transform: translateZ(-80px);
transform: translateZ(-80px);
opacity: 0;
}
}
@media (prefers-color-scheme: dark) {
a.item,
nav,
.aplayer .aplayer-info .aplayer-music .aplayer-title,
.download-button,
.loading-label,
.markdown-body,
.markdown-body code,
.markdown-body pre,
.markdown-body .highlight pre,
.markdown-body table td,
.markdown-body table th,
.paginate-container span,
.path a {
color: #a2a2a2 !important;
}
body,
div.fade-in-fwd,
div.intro,
div.medium-zoom-overlay,
embed.pdfobject,
img.medium-zoom-image {
background: #1b1b1b !important;
}
embed.pdfobject,
img.medium-zoom-image {
opacity: 0.75 !important;
}
nav,
.aplayer .aplayer-info,
.items,
.markdown-body,
.markdown-body img {
background: #131313 !important;
}
.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon:hover path {
fill: #a2a2a2 !important;
}
.aplayer .aplayer-info .aplayer-music .aplayer-author,
.path {
color: #626262 !important;
}
.markdown-body code,
.markdown-body pre,
.markdown-body .highlight pre,
.markdown-body table td,
.markdown-body table th {
background: #2b2b2b !important;
}
.token.atrule,
.token.attr-value,
.token.punctuation,
.token.string {
color: #afd5fb !important;
}
.language-css .token.string,
.token.entity,
.token.operator,
.token.url {
color: #89befa !important;
}
}
.upload-titile{
text-align: center;
}

11
wrangler.toml Normal file
View File

@ -0,0 +1,11 @@
name = "drive"
type = "webpack"
workers_dev = true
account_id = "8a9144b242785d876d548e0c4349dae5"
zone_id = "45cb44c4fe092370e7dea3f22eb1fd44"
kv_namespaces = [
{ binding = "BUCKET", id = "49008a8661914520b6ee073ae284ce82", preview_id = "b1bbcd336a5b495dade81e64bc6e5c70" }
]
[env.staging]
name = "drive-staginga"