mirror of
https://github.com/MarSeventh/CloudFlare-ImgBed.git
synced 2026-01-31 09:03:19 +08:00
Feat:支持WebDAV功能
This commit is contained in:
110
README.md
110
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/MarSeventh/CloudFlare-ImgBed"><img width="80%" alt="logo" src="static/readme/banner.png"/></a>
|
||||
<p><em>🗂️开源文件托管解决方案,支持 Docker 和无服务器部署,支持 Telegram Bot 、 Cloudflare R2 、S3 等多种存储渠道</em></p>
|
||||
<p><em>🗂️开源文件托管解决方案,支持 Docker 和无服务器部署,支持 Telegram Bot 、 Cloudflare R2 、S3 等多种存储渠道,支持 WebDAV 协议和多种 RESTful API</em></p>
|
||||
<p>
|
||||
<a href="https://github.com/MarSeventh/CloudFlare-ImgBed/blob/main/README.md">简体中文</a> | <a href="https://github.com/MarSeventh/CloudFlare-ImgBed/blob/main/README_en.md">English</a> | <a href="https://cfbed.sanyue.de">官方网站</a>
|
||||
</p>
|
||||
@@ -111,115 +111,9 @@
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
# 4. WebDAV Bridge 桥接服务
|
||||
|
||||
本项目提供了一个强大的 **WebDAV Bridge Cloudflare Worker**,让您可以通过标准的 WebDAV 协议访问和管理托管的文件。
|
||||
|
||||
## 4.1 功能特性
|
||||
|
||||
- 🔒 **身份验证**:支持基于用户名密码的 Basic Auth 认证
|
||||
- 📁 **目录浏览**:完整的目录结构展示,支持 HTML 页面和 WebDAV 客户端
|
||||
- 📤 **文件上传**:通过 PUT 方法上传文件到指定目录
|
||||
- 🗑️ **文件删除**:支持删除单个文件或整个文件夹
|
||||
- 📥 **文件下载**:直接下载文件,自动代理到上游存储
|
||||
- 🌐 **跨域支持**:内置 CORS 支持,确保 Web 客户端正常访问
|
||||
|
||||
## 4.2 支持的 WebDAV 方法
|
||||
|
||||
| 方法 | 功能 | 说明 |
|
||||
|------|------|------|
|
||||
| `PROPFIND` | 列出目录内容 | 获取文件和文件夹列表,支持 WebDAV 客户端 |
|
||||
| `GET` | 下载文件/浏览目录 | 文件下载或 HTML 目录浏览页面 |
|
||||
| `PUT` | 上传文件 | 上传文件到指定路径和文件夹 |
|
||||
| `DELETE` | 删除文件/文件夹 | 支持删除单个文件或整个目录 |
|
||||
| `OPTIONS` | 协议探测 | 返回支持的 WebDAV 方法和功能 |
|
||||
| `MKCOL` | 创建目录 | 创建新的文件夹(自动支持) |
|
||||
|
||||
## 4.3 部署配置
|
||||
|
||||
### 4.3.1 环境变量设置
|
||||
|
||||
需要在 Cloudflare Worker 中设置以下环境变量:
|
||||
|
||||
```bash
|
||||
# WebDAV 认证凭据
|
||||
AUTH_USER=your_username # WebDAV 登录用户名
|
||||
AUTH_PASS=your_password # WebDAV 登录密码
|
||||
|
||||
# 上游 API 配置
|
||||
UPSTREAM_HOST=your-imgbed.domain.com # 您的图床域名
|
||||
API_TOKEN=your_api_token # API 访问令牌
|
||||
```
|
||||
|
||||
### 4.3.2 自定义域名绑定(推荐)
|
||||
|
||||
为了获得更好的使用体验,强烈建议为 WebDAV Worker 绑定自定义域名:
|
||||
|
||||
1. **准备域名**:确保您有一个可用的域名,并且该域名已托管在 Cloudflare
|
||||
2. **添加自定义路由**:
|
||||
- 进入 Cloudflare Workers 控制台
|
||||
- 选择您的 WebDAV Worker
|
||||
- 点击 `触发器` (Triggers) 标签
|
||||
- 点击 `添加自定义域名`
|
||||
- 输入您的子域名,如:`webdav.yourdomain.com`
|
||||
- 点击 `添加域名`
|
||||
|
||||
3. **SSL 证书**:Cloudflare 会自动为您的自定义域名提供免费 SSL 证书
|
||||
|
||||
**使用自定义域名的优势**:
|
||||
- 🌟 **更好的兼容性**:避免某些 WebDAV 客户端对 `.workers.dev` 域名的限制
|
||||
- 🔒 **更高的安全性**:自定义域名通常更受客户端信任
|
||||
- 📱 **移动端友好**:iOS/Android 设备对自定义域名支持更好
|
||||
- 🎯 **品牌一致性**:与您的图床服务使用统一的域名体系
|
||||
|
||||
## 4.4 使用方式
|
||||
|
||||
### 浏览器访问
|
||||
直接在浏览器中访问 Worker 地址,输入认证信息后可以浏览文件目录:
|
||||
```
|
||||
# 使用自定义域名(推荐)
|
||||
https://webdav.yourdomain.com/
|
||||
|
||||
# 或使用默认 Worker 域名
|
||||
https://your-webdav-worker.your-subdomain.workers.dev/
|
||||
```
|
||||
|
||||
### WebDAV 客户端
|
||||
可以使用任何支持 WebDAV 的客户端连接:
|
||||
|
||||
**Windows 资源管理器**:
|
||||
1. 打开"此电脑"
|
||||
2. 右键选择"添加网络位置"
|
||||
3. 输入 WebDAV Worker 地址
|
||||
4. 输入用户名和密码
|
||||
|
||||
**macOS Finder**:
|
||||
1. 在 Finder 中按 `Cmd+K`
|
||||
2. 输入 WebDAV 地址(推荐使用自定义域名):
|
||||
- `https://webdav.yourdomain.com` 或
|
||||
- `https://your-webdav-worker.your-subdomain.workers.dev`
|
||||
3. 输入认证信息
|
||||
|
||||
**第三方客户端**:
|
||||
- Cyberduck、WinSCP、FileZilla Pro 等文件管理器
|
||||
- Mobile 端:FE File Explorer、Documents by Readdle 等
|
||||
|
||||
## 4.5 特色功能
|
||||
|
||||
- **智能路径处理**:自动处理文件路径,支持中文和特殊字符
|
||||
- **分页加载**:大目录自动分页加载,提升性能
|
||||
- **错误处理**:完善的错误处理和用户友好的错误信息
|
||||
- **缓存优化**:合理利用浏览器缓存,提升访问速度
|
||||
- **安全可靠**:基于 Cloudflare Worker 的边缘计算,全球加速
|
||||
|
||||
通过 WebDAV Bridge,您可以像使用本地文件夹一样管理托管的文件,实现了真正的"云端硬盘"体验!
|
||||
|
||||
# 5. Tips
|
||||
# 4. Tips
|
||||
|
||||
- **前端开源**:参见[MarSeventh/Sanyue-ImgHub](https://github.com/MarSeventh/Sanyue-ImgHub)项目。
|
||||
|
||||
|
||||
107
README_en.md
107
README_en.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/MarSeventh/CloudFlare-ImgBed"><img width="80%" alt="logo" src="static/readme/banner.png"/></a>
|
||||
<p><em>🗂️Open-source file hosting solution, supporting Docker and serverless deployment, supporting multiple storage channels such as Telegram Bot, Cloudflare R2, S3, etc.</em></p>
|
||||
<p><em>🗂️Open-source file hosting solution, supporting Docker and serverless deployment, supporting multiple storage channels such as Telegram Bot, Cloudflare R2, S3, etc., supporting WebDAV protocol and various RESTful APIs.</em></p>
|
||||
<p>
|
||||
<a href="https://github.com/MarSeventh/CloudFlare-ImgBed/blob/main/README.md">简体中文</a> | <a href="https://github.com/MarSeventh/CloudFlare-ImgBed/blob/main/README_en.md">English</a> | <a
|
||||
href="https://cfbed.sanyue.de/en">Official Website</a>
|
||||
@@ -102,110 +102,7 @@ Provides detailed deployment documentation, feature docs, development plans, upd
|
||||
|
||||
</details>
|
||||
|
||||
# 4. WebDAV Bridge Service
|
||||
|
||||
This project provides a powerful **WebDAV Bridge Cloudflare Worker** that allows you to access and manage hosted files through the standard WebDAV protocol.
|
||||
|
||||
## 4.1 Features
|
||||
|
||||
- 🔒 **Authentication**: Supports Basic Auth authentication with username/password
|
||||
- 📁 **Directory Browsing**: Complete directory structure display, supports both HTML pages and WebDAV clients
|
||||
- 📤 **File Upload**: Upload files to specified directories using PUT method
|
||||
- 🗑️ **File Deletion**: Support for deleting individual files or entire folders
|
||||
- 📥 **File Download**: Direct file downloads with automatic proxy to upstream storage
|
||||
- 🌐 **CORS Support**: Built-in CORS support ensuring proper web client access
|
||||
|
||||
## 4.2 Supported WebDAV Methods
|
||||
|
||||
| Method | Function | Description |
|
||||
|--------|----------|-------------|
|
||||
| `PROPFIND` | List directory contents | Get file and folder lists, supports WebDAV clients |
|
||||
| `GET` | Download file/browse directory | File downloads or HTML directory browsing pages |
|
||||
| `PUT` | Upload file | Upload files to specified paths and folders |
|
||||
| `DELETE` | Delete file/folder | Support for deleting individual files or entire directories |
|
||||
| `OPTIONS` | Protocol detection | Returns supported WebDAV methods and features |
|
||||
| `MKCOL` | Create directory | Create new folders (automatically supported) |
|
||||
|
||||
## 4.3 Deployment Configuration
|
||||
|
||||
### 4.3.1 Environment Variables
|
||||
|
||||
Set the following environment variables in your Cloudflare Worker:
|
||||
|
||||
```bash
|
||||
# WebDAV authentication credentials
|
||||
AUTH_USER=your_username # WebDAV login username
|
||||
AUTH_PASS=your_password # WebDAV login password
|
||||
|
||||
# Upstream API configuration
|
||||
UPSTREAM_HOST=your-imgbed.domain.com # Your image bed domain
|
||||
API_TOKEN=your_api_token # API access token
|
||||
```
|
||||
|
||||
### 4.3.2 Custom Domain Binding (Recommended)
|
||||
|
||||
For a better user experience, it's highly recommended to bind a custom domain to your WebDAV Worker:
|
||||
|
||||
1. **Prepare Domain**: Ensure you have an available domain that is hosted on Cloudflare
|
||||
2. **Add Custom Route**:
|
||||
- Go to Cloudflare Workers console
|
||||
- Select your WebDAV Worker
|
||||
- Click the `Triggers` tab
|
||||
- Click `Add Custom Domain`
|
||||
- Enter your subdomain, e.g.: `webdav.yourdomain.com`
|
||||
- Click `Add Domain`
|
||||
|
||||
3. **SSL Certificate**: Cloudflare will automatically provide a free SSL certificate for your custom domain
|
||||
|
||||
**Advantages of Using Custom Domain**:
|
||||
- 🌟 **Better Compatibility**: Avoid limitations some WebDAV clients have with `.workers.dev` domains
|
||||
- 🔒 **Enhanced Security**: Custom domains are generally more trusted by clients
|
||||
- 📱 **Mobile Friendly**: iOS/Android devices have better support for custom domains
|
||||
- 🎯 **Brand Consistency**: Use a unified domain system with your image hosting service
|
||||
|
||||
## 4.4 Usage
|
||||
|
||||
### Browser Access
|
||||
Access the Worker address directly in your browser and enter authentication credentials to browse file directories:
|
||||
```
|
||||
# Using custom domain (recommended)
|
||||
https://webdav.yourdomain.com/
|
||||
|
||||
# Or using default Worker domain
|
||||
https://your-webdav-worker.your-subdomain.workers.dev/
|
||||
```
|
||||
|
||||
### WebDAV Clients
|
||||
You can use any WebDAV-compatible client to connect:
|
||||
|
||||
**Windows File Explorer**:
|
||||
1. Open "This PC"
|
||||
2. Right-click and select "Add a network location"
|
||||
3. Enter the WebDAV Worker address
|
||||
4. Enter username and password
|
||||
|
||||
**macOS Finder**:
|
||||
1. Press `Cmd+K` in Finder
|
||||
2. Enter WebDAV address (custom domain recommended):
|
||||
- `https://webdav.yourdomain.com` or
|
||||
- `https://your-webdav-worker.your-subdomain.workers.dev`
|
||||
3. Enter authentication credentials
|
||||
|
||||
**Third-party Clients**:
|
||||
- File managers like Cyberduck, WinSCP, FileZilla Pro
|
||||
- Mobile: FE File Explorer, Documents by Readdle, etc.
|
||||
|
||||
## 4.5 Key Features
|
||||
|
||||
- **Smart Path Handling**: Automatic path processing with support for Chinese characters and special characters
|
||||
- **Paginated Loading**: Large directories are automatically paginated for improved performance
|
||||
- **Error Handling**: Comprehensive error handling with user-friendly error messages
|
||||
- **Cache Optimization**: Proper browser caching utilization for improved access speed
|
||||
- **Secure & Reliable**: Based on Cloudflare Worker edge computing with global acceleration
|
||||
|
||||
Through WebDAV Bridge, you can manage hosted files just like using a local folder, achieving a true "cloud drive" experience!
|
||||
|
||||
# 5. Tips
|
||||
# 4. Tips
|
||||
|
||||
- Frontend is open source, see [MarSeventh/Sanyue-ImgHub](https://github.com/MarSeventh/Sanyue-ImgHub).
|
||||
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
// Cloudflare Worker: WebDAV Bridge (v8 - Final version with /file/ prefix for downloads)
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const authResponse = checkAuth(request, env);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
switch (request.method) {
|
||||
case 'OPTIONS': return handleOptions(request);
|
||||
case 'PROPFIND': return handlePropfind(request, env);
|
||||
case 'PUT': return handlePut(request, env);
|
||||
case 'DELETE': return handleDelete(request, env);
|
||||
case 'GET': return handleGet(request, env);
|
||||
case 'MKCOL': return new Response(null, { status: 201 });
|
||||
default: return new Response('Method Not Allowed', { status: 405 });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// --- UTILITY FUNCTIONS ---
|
||||
|
||||
function getApiHeaders(env) {
|
||||
return {
|
||||
'Authorization': `Bearer ${env.API_TOKEN}`,
|
||||
'User-Agent': 'Cloudflare-WebDAV-Worker'
|
||||
};
|
||||
}
|
||||
|
||||
function checkAuth(request, env) {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return new Response('Authorization required', {
|
||||
status: 401,
|
||||
headers: { 'WWW-Authenticate': 'Basic realm="WebDAV"' },
|
||||
});
|
||||
}
|
||||
const [scheme, encoded] = authHeader.split(' ');
|
||||
if (scheme !== 'Basic' || !encoded) {
|
||||
return new Response('Malformed Authorization header', { status: 400 });
|
||||
}
|
||||
const [user, pass] = atob(encoded).split(':');
|
||||
if (user !== env.AUTH_USER || pass !== env.AUTH_PASS) {
|
||||
return new Response('Invalid credentials', { status: 403 });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- WEBDAV METHOD HANDLERS ---
|
||||
|
||||
function handleOptions(request) {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Allow': 'OPTIONS, GET, PUT, DELETE, PROPFIND, MKCOL',
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleGet(request, env) {
|
||||
const path = decodeURIComponent(new URL(request.url).pathname);
|
||||
|
||||
if (path.endsWith('/')) { // Directory listing
|
||||
try {
|
||||
const dir = path === '/' ? '' : path.substring(1, path.length - 1);
|
||||
const contents = await fetchDirectoryContents(dir, env);
|
||||
const html = generateDirectoryListingHtml(path, contents);
|
||||
return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||
} catch (error) {
|
||||
console.error('GET (directory) failed:', error.stack);
|
||||
return new Response(`Error listing directory: ${error.message}`, { status: 500 });
|
||||
}
|
||||
} else { // File download
|
||||
try {
|
||||
// **FINAL FIX:** Added the mandatory /file/ prefix for the upstream URL.
|
||||
// The `path` variable already includes the leading slash (e.g., /folder/image.jpg).
|
||||
const fileUrl = `https://${env.UPSTREAM_HOST}/file${path}`;
|
||||
|
||||
const fileResponse = await fetch(fileUrl);
|
||||
if (!fileResponse.ok) {
|
||||
return new Response('File not found', { status: fileResponse.status, statusText: fileResponse.statusText });
|
||||
}
|
||||
const response = new Response(fileResponse.body, fileResponse);
|
||||
response.headers.set('Access-Control-Allow-Origin', '*');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('GET (file) failed:', error.stack);
|
||||
return new Response(`Error getting file: ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePut(request, env) {
|
||||
const fullPath = decodeURIComponent(new URL(request.url).pathname.substring(1));
|
||||
if (!fullPath || fullPath.endsWith('/')) {
|
||||
return new Response('Invalid file name', { status: 400 });
|
||||
}
|
||||
|
||||
const lastSlashIndex = fullPath.lastIndexOf('/');
|
||||
const uploadFolder = lastSlashIndex > -1 ? fullPath.substring(0, lastSlashIndex) : '';
|
||||
const fileName = lastSlashIndex > -1 ? fullPath.substring(lastSlashIndex + 1) : fullPath;
|
||||
|
||||
const fileContent = await request.blob();
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileContent, fileName);
|
||||
|
||||
const uploadUrl = new URL(`https://${env.UPSTREAM_HOST}/upload`);
|
||||
if (uploadFolder) {
|
||||
uploadUrl.searchParams.set('uploadFolder', uploadFolder);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(uploadUrl.toString(), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: getApiHeaders(env)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok && Array.isArray(result) && result.length > 0 && result[0].src) {
|
||||
return new Response(null, { status: 201 }); // Created
|
||||
} else {
|
||||
const errorMsg = result.error || JSON.stringify(result);
|
||||
console.error('Upload API error:', errorMsg);
|
||||
return new Response(`Upload failed: ${errorMsg}`, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch to upload API failed:', error.stack);
|
||||
return new Response('Failed to contact upload service', { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(request, env) {
|
||||
const path = decodeURIComponent(new URL(request.url).pathname.substring(1));
|
||||
if (!path) return new Response('Invalid path for DELETE', { status: 400 });
|
||||
|
||||
const isFolder = path.endsWith('/');
|
||||
const cleanPath = isFolder ? path.slice(0, -1) : path;
|
||||
|
||||
const deleteUrl = new URL(`https://${env.UPSTREAM_HOST}/api/manage/delete/${cleanPath}`);
|
||||
if (isFolder) deleteUrl.searchParams.set('folder', 'true');
|
||||
|
||||
try {
|
||||
const response = await fetch(deleteUrl.toString(), {
|
||||
method: 'DELETE',
|
||||
headers: getApiHeaders(env)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return new Response(null, { status: 204 }); // No Content
|
||||
} else {
|
||||
console.error('Delete API error:', JSON.stringify(result));
|
||||
return new Response(`Deletion failed: ${result.error || 'API error'}`, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete operation failed:', error.stack);
|
||||
return new Response(`Internal server error: ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePropfind(request, env) {
|
||||
const path = decodeURIComponent(new URL(request.url).pathname);
|
||||
try {
|
||||
const dir = path === '/' ? '' : path.substring(1, path.endsWith('/') ? path.length - 1 : path.length);
|
||||
const contents = await fetchDirectoryContents(dir, env);
|
||||
const xml = generateWebDAVXml(path, contents);
|
||||
return new Response(xml, { status: 207, headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
|
||||
} catch (error) {
|
||||
console.error('Propfind failed:', error.stack);
|
||||
return new Response(`Failed to list files: ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// --- API DATA FETCHING ---
|
||||
|
||||
async function fetchDirectoryContents(dir, env) {
|
||||
let allFiles = [];
|
||||
let allDirectories = [];
|
||||
let start = 0;
|
||||
const count = 100;
|
||||
|
||||
while (true) {
|
||||
const listUrl = new URL(`https://${env.UPSTREAM_HOST}/api/manage/list`);
|
||||
listUrl.searchParams.set('dir', dir);
|
||||
listUrl.searchParams.set('start', start);
|
||||
listUrl.searchParams.set('count', count);
|
||||
|
||||
const response = await fetch(listUrl.toString(), { headers: getApiHeaders(env) });
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`API fetch error: Status ${response.status} - ${errorText}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.error) {
|
||||
throw new Error(`API error: ${result.error} - ${result.message}`);
|
||||
}
|
||||
|
||||
if (result.files && result.files.length > 0) allFiles = allFiles.concat(result.files);
|
||||
if (result.directories && result.directories.length > 0) allDirectories = allDirectories.concat(result.directories);
|
||||
if (!result.files || result.files.length < count) break;
|
||||
start += count;
|
||||
}
|
||||
return { files: allFiles, directories: [...new Set(allDirectories)] };
|
||||
}
|
||||
|
||||
// --- HTML and XML GENERATION ---
|
||||
|
||||
function generateDirectoryListingHtml(basePath, contents) {
|
||||
let fileLinks = '';
|
||||
let dirLinks = '';
|
||||
|
||||
for (const dir of contents.directories) {
|
||||
const fullDirPath = `/${dir}/`;
|
||||
const dirName = dir.split('/').pop();
|
||||
dirLinks += `<li><a href="${fullDirPath}"><strong>${dirName}/</strong></a></li>`;
|
||||
}
|
||||
|
||||
for (const file of contents.files) {
|
||||
const fullFilePath = `/${file.name}`;
|
||||
const fileName = file.name.split('/').pop();
|
||||
const fileSize = file.metadata && file.metadata['File-Size']
|
||||
? `${Math.round(parseInt(file.metadata['File-Size']) / 1024)} KB`
|
||||
: 'N/A';
|
||||
fileLinks += `<li><a href="${fullFilePath}">${fileName}</a> - ${fileSize}</li>`;
|
||||
}
|
||||
|
||||
let parentDirLink = '';
|
||||
if (basePath !== '/') {
|
||||
const parentPath = new URL('..', `http://dummy.com${basePath}`).pathname;
|
||||
parentDirLink = `<li><a href="${parentPath}"><strong>../ (Parent Directory)</strong></a></li>`;
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html><html><head><title>Index of ${basePath}</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{font-family:sans-serif;padding:20px}li{margin:5px 0}</style></head><body><h1>Index of ${basePath}</h1><ul>${parentDirLink}${dirLinks}${fileLinks}</ul></body></html>`;
|
||||
}
|
||||
|
||||
function generateWebDAVXml(basePath, contents) {
|
||||
let responses = '';
|
||||
const currentPath = basePath.endsWith('/') ? basePath : `${basePath}/`;
|
||||
|
||||
responses += createCollectionXml(currentPath);
|
||||
|
||||
for (const dir of contents.directories) {
|
||||
responses += createCollectionXml(`/${dir}/`);
|
||||
}
|
||||
for (const file of contents.files) {
|
||||
responses += createFileXml(file);
|
||||
}
|
||||
return `<?xml version="1.0" encoding="utf-8"?><D:multistatus xmlns:D="DAV:">${responses}</D:multistatus>`;
|
||||
}
|
||||
|
||||
function createCollectionXml(path) {
|
||||
const now = new Date().toUTCString();
|
||||
const cleanPath = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
const name = cleanPath.split('/').pop() || '';
|
||||
return `<D:response><D:href>${encodeURI(path)}</D:href><D:propstat><D:prop><D:displayname>${name}</D:displayname><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>${now}</D:creationdate><D:getlastmodified>${now}</D:getlastmodified></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>`;
|
||||
}
|
||||
|
||||
function createFileXml(file) {
|
||||
const now = new Date().toUTCString();
|
||||
const fileSize = file.metadata && file.metadata['File-Size'] ? file.metadata['File-Size'] : "0";
|
||||
return `<D:response><D:href>${encodeURI(`/${file.name}`)}</D:href><D:propstat><D:prop><D:displayname>${file.name.split('/').pop()}</D:displayname><D:resourcetype/><D:creationdate>${now}</D:creationdate><D:getlastmodified>${now}</D:getlastmodified><D:getcontentlength>${fileSize}</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>`;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
css/936.973d7489.css.gz
Normal file
BIN
css/936.973d7489.css.gz
Normal file
Binary file not shown.
Binary file not shown.
@@ -71,6 +71,15 @@ export async function getOthersConfig(db, env) {
|
||||
fixed: false,
|
||||
}
|
||||
|
||||
// WebDAV
|
||||
const kvWebDAV = settingsKV.webDAV || {}
|
||||
settings.webDAV = {
|
||||
enabled: kvWebDAV.enabled ?? false,
|
||||
username: kvWebDAV.username || '',
|
||||
password: kvWebDAV.password || '',
|
||||
fixed: false,
|
||||
}
|
||||
|
||||
|
||||
return settings;
|
||||
}
|
||||
293
functions/dav/[[path]].js
Normal file
293
functions/dav/[[path]].js
Normal file
@@ -0,0 +1,293 @@
|
||||
// WebDAV 服务支持
|
||||
import { fetchSecurityConfig, fetchOthersConfig } from "../utils/sysConfig";
|
||||
|
||||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
|
||||
const authResponse = await checkAuth(request, env);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
// 从请求路径中替换第一个 /dav 部分
|
||||
const url = new URL(request.url);
|
||||
url.pathname = url.pathname.replace(/^\/dav/, '') || '/';
|
||||
const modifiedRequest = new Request(url.toString(), request);
|
||||
|
||||
switch (modifiedRequest.method) {
|
||||
case 'OPTIONS': return handleOptions(modifiedRequest);
|
||||
case 'PROPFIND': return handlePropfind(modifiedRequest, env);
|
||||
case 'PUT': return handlePut(modifiedRequest, env);
|
||||
case 'DELETE': return handleDelete(modifiedRequest, env);
|
||||
case 'GET': return handleGet(modifiedRequest, env);
|
||||
case 'MKCOL': return new Response(null, { status: 201 });
|
||||
default: return new Response('Method Not Allowed', { status: 405 });
|
||||
}
|
||||
}
|
||||
|
||||
// --- UTILITY FUNCTIONS ---
|
||||
|
||||
async function getApiHeaders(env) {
|
||||
const securityConfig = await fetchSecurityConfig(env);
|
||||
|
||||
const adminUsername = securityConfig.auth.admin.adminUsername;
|
||||
const adminPassword = securityConfig.auth.admin.adminPassword;
|
||||
const authCode = securityConfig.auth.user.authCode;
|
||||
|
||||
let credentials = btoa('unset:unset');
|
||||
|
||||
if (adminUsername && adminPassword) {
|
||||
credentials = btoa(`${adminUsername}:${adminPassword}`);
|
||||
}
|
||||
|
||||
return {
|
||||
'Authorization': `Basic ${credentials}`,
|
||||
'authCode': authCode || ''
|
||||
};
|
||||
}
|
||||
|
||||
async function checkAuth(request, env) {
|
||||
const othersConfig = await fetchOthersConfig(env);
|
||||
|
||||
const enabled = othersConfig.webDAV.enabled;
|
||||
if (!enabled) return new Response('WebDAV is disabled', { status: 403 }); // WebDAV disabled
|
||||
|
||||
const davUser = othersConfig.webDAV.username;
|
||||
const davPass = othersConfig.webDAV.password;
|
||||
if (!davUser || !davPass) return null; // No auth required
|
||||
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return new Response('Authorization required', {
|
||||
status: 401,
|
||||
headers: { 'WWW-Authenticate': 'Basic realm="WebDAV"' },
|
||||
});
|
||||
}
|
||||
|
||||
const [scheme, encoded] = authHeader.split(' ');
|
||||
if (scheme !== 'Basic' || !encoded) {
|
||||
return new Response('Malformed Authorization header', { status: 400 });
|
||||
}
|
||||
|
||||
const [user, pass] = atob(encoded).split(':');
|
||||
if (user !== davUser || pass !== davPass) {
|
||||
return new Response('Invalid credentials', { status: 403 });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- WEBDAV METHOD HANDLERS ---
|
||||
|
||||
function handleOptions(request) {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Allow': 'OPTIONS, GET, PUT, DELETE, PROPFIND, MKCOL',
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleGet(request, env) {
|
||||
const path = decodeURIComponent(new URL(request.url).pathname);
|
||||
|
||||
if (path.endsWith('/')) { // Directory listing
|
||||
try {
|
||||
const dir = path === '/' ? '' : path.substring(1, path.length - 1);
|
||||
const contents = await fetchDirectoryContents(dir, env, request);
|
||||
const html = generateDirectoryListingHtml(path, contents);
|
||||
return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||
} catch (error) {
|
||||
console.error('GET (directory) failed:', error.stack);
|
||||
return new Response(`Error listing directory: ${error.message}`, { status: 500 });
|
||||
}
|
||||
} else { // File download
|
||||
try {
|
||||
const fileUrl = new URL(`/file${path}`, request.url);
|
||||
|
||||
const fileResponse = await fetch(fileUrl.toString());
|
||||
|
||||
if (!fileResponse.ok) {
|
||||
return new Response('File not found', { status: fileResponse.status, statusText: fileResponse.statusText });
|
||||
}
|
||||
|
||||
const response = new Response(fileResponse.body, fileResponse);
|
||||
response.headers.set('Access-Control-Allow-Origin', '*');
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('GET (file) failed:', error.stack);
|
||||
return new Response(`Error getting file: ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePut(request, env) {
|
||||
const fullPath = decodeURIComponent(new URL(request.url).pathname.substring(1));
|
||||
if (!fullPath || fullPath.endsWith('/')) {
|
||||
return new Response('Invalid file name', { status: 400 });
|
||||
}
|
||||
|
||||
const lastSlashIndex = fullPath.lastIndexOf('/');
|
||||
const uploadFolder = lastSlashIndex > -1 ? fullPath.substring(0, lastSlashIndex) : '';
|
||||
const fileName = lastSlashIndex > -1 ? fullPath.substring(lastSlashIndex + 1) : fullPath;
|
||||
|
||||
const fileContent = await request.blob();
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileContent, fileName);
|
||||
|
||||
const uploadUrl = new URL(`/upload`, request.url);
|
||||
if (uploadFolder) {
|
||||
uploadUrl.searchParams.set('uploadFolder', uploadFolder);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(uploadUrl.toString(), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: await getApiHeaders(env)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok && Array.isArray(result) && result.length > 0 && result[0].src) {
|
||||
return new Response(null, { status: 201 }); // Created
|
||||
} else {
|
||||
const errorMsg = result.error || JSON.stringify(result);
|
||||
console.error('Upload API error:', errorMsg);
|
||||
return new Response(`Upload failed: ${errorMsg}`, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch to upload API failed:', error.stack);
|
||||
return new Response('Failed to contact upload service', { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(request, env) {
|
||||
const path = decodeURIComponent(new URL(request.url).pathname.substring(1));
|
||||
if (!path) return new Response('Invalid path for DELETE', { status: 400 });
|
||||
|
||||
const isFolder = path.endsWith('/');
|
||||
const cleanPath = isFolder ? path.slice(0, -1) : path;
|
||||
|
||||
const deleteUrl = new URL(`/api/manage/delete/${cleanPath}`, request.url);
|
||||
if (isFolder) deleteUrl.searchParams.set('folder', 'true');
|
||||
|
||||
try {
|
||||
const response = await fetch(deleteUrl.toString(), {
|
||||
method: 'DELETE',
|
||||
headers: await getApiHeaders(env)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return new Response(null, { status: 204 }); // No Content
|
||||
} else {
|
||||
console.error('Delete API error:', JSON.stringify(result));
|
||||
return new Response(`Deletion failed: ${result.error || 'API error'}`, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete operation failed:', error.stack);
|
||||
return new Response(`Internal server error: ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePropfind(request, env) {
|
||||
const path = decodeURIComponent(new URL(request.url).pathname);
|
||||
try {
|
||||
const dir = path === '/' ? '' : path.substring(1, path.endsWith('/') ? path.length - 1 : path.length);
|
||||
const contents = await fetchDirectoryContents(dir, env, request);
|
||||
const xml = generateWebDAVXml(path, contents);
|
||||
return new Response(xml, { status: 207, headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
|
||||
} catch (error) {
|
||||
console.error('Propfind failed:', error.stack);
|
||||
return new Response(`Failed to list files: ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// --- API DATA FETCHING ---
|
||||
|
||||
async function fetchDirectoryContents(dir, env, request) {
|
||||
let allFiles = [];
|
||||
let allDirectories = [];
|
||||
let start = 0;
|
||||
const count = 100;
|
||||
|
||||
while (true) {
|
||||
const listUrl = new URL(`/api/manage/list`, request.url);
|
||||
listUrl.searchParams.set('dir', dir);
|
||||
listUrl.searchParams.set('start', start);
|
||||
listUrl.searchParams.set('count', count);
|
||||
|
||||
const response = await fetch(listUrl.toString(), { headers: await getApiHeaders(env) });
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`API fetch error: Status ${response.status} - ${errorText}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.error) {
|
||||
throw new Error(`API error: ${result.error} - ${result.message}`);
|
||||
}
|
||||
|
||||
if (result.files && result.files.length > 0) allFiles = allFiles.concat(result.files);
|
||||
if (result.directories && result.directories.length > 0) allDirectories = allDirectories.concat(result.directories);
|
||||
if (!result.files || result.files.length < count) break;
|
||||
start += count;
|
||||
}
|
||||
return { files: allFiles, directories: [...new Set(allDirectories)] };
|
||||
}
|
||||
|
||||
// --- HTML and XML GENERATION ---
|
||||
|
||||
function generateDirectoryListingHtml(basePath, contents) {
|
||||
let fileLinks = '';
|
||||
let dirLinks = '';
|
||||
|
||||
for (const dir of contents.directories) {
|
||||
const fullDirPath = `/dav/${dir}/`;
|
||||
const dirName = dir.split('/').pop();
|
||||
dirLinks += `<li><a href="${fullDirPath}"><strong>${dirName}/</strong></a></li>`;
|
||||
}
|
||||
|
||||
for (const file of contents.files) {
|
||||
const fullFilePath = `/dav/${file.name}`;
|
||||
const fileName = file.name.split('/').pop();
|
||||
const fileSize = file.metadata && file.metadata['FileSize']
|
||||
? `${file.metadata['FileSize']} MB`
|
||||
: 'N/A';
|
||||
fileLinks += `<li><a href="${fullFilePath}">${fileName}</a> - ${fileSize}</li>`;
|
||||
}
|
||||
|
||||
let parentDirLink = '';
|
||||
if (basePath !== '/') {
|
||||
const parentPath = new URL('..', `http://dummy.com${basePath}`).pathname;
|
||||
parentDirLink = `<li><a href="/dav${parentPath}"><strong>../ (Parent Directory)</strong></a></li>`;
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html><html><head><title>Index of ${basePath}</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{font-family:sans-serif;padding:20px}li{margin:5px 0}</style></head><body><h1>Index of ${basePath}</h1><ul>${parentDirLink}${dirLinks}${fileLinks}</ul></body></html>`;
|
||||
}
|
||||
|
||||
function generateWebDAVXml(basePath, contents) {
|
||||
let responses = '';
|
||||
const currentPath = basePath.endsWith('/') ? basePath : `${basePath}/`;
|
||||
|
||||
responses += createCollectionXml(currentPath);
|
||||
|
||||
for (const dir of contents.directories) {
|
||||
responses += createCollectionXml(`/${dir}/`);
|
||||
}
|
||||
for (const file of contents.files) {
|
||||
responses += createFileXml(file);
|
||||
}
|
||||
return `<?xml version="1.0" encoding="utf-8"?><D:multistatus xmlns:D="DAV:">${responses}</D:multistatus>`;
|
||||
}
|
||||
|
||||
function createCollectionXml(path) {
|
||||
const now = new Date().toUTCString();
|
||||
const cleanPath = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
const name = cleanPath.split('/').pop() || '';
|
||||
return `<D:response><D:href>${encodeURI(path)}</D:href><D:propstat><D:prop><D:displayname>${name}</D:displayname><D:resourcetype><D:collection/></D:resourcetype><D:creationdate>${now}</D:creationdate><D:getlastmodified>${now}</D:getlastmodified></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>`;
|
||||
}
|
||||
|
||||
function createFileXml(file) {
|
||||
const now = new Date().toUTCString();
|
||||
const fileSize = file.metadata && file.metadata['File-Size'] ? file.metadata['File-Size'] : "0";
|
||||
return `<D:response><D:href>${encodeURI(`/${file.name}`)}</D:href><D:propstat><D:prop><D:displayname>${file.name.split('/').pop()}</D:displayname><D:resourcetype/><D:creationdate>${now}</D:creationdate><D:getlastmodified>${now}</D:getlastmodified><D:getcontentlength>${fileSize}</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>`;
|
||||
}
|
||||
3
functions/dav/_middleware.js
Normal file
3
functions/dav/_middleware.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { checkDatabaseConfig } from '../utils/middleware';
|
||||
|
||||
export const onRequest = [checkDatabaseConfig];
|
||||
@@ -11,15 +11,15 @@ import { D1Database } from './d1Database.js';
|
||||
* @returns {Object} 数据库适配器实例
|
||||
*/
|
||||
export function createDatabaseAdapter(env) {
|
||||
// 检查是否配置了D1数据库
|
||||
if (env.img_d1 && typeof env.img_d1.prepare === 'function') {
|
||||
// 检查是否配置了数据库
|
||||
if (env.img_url && typeof env.img_url.get === 'function') {
|
||||
// 使用KV存储
|
||||
return new KVAdapter(env.img_url);
|
||||
} else if (env.img_d1 && typeof env.img_d1.prepare === 'function') {
|
||||
// 使用D1数据库
|
||||
return new D1Database(env.img_d1);
|
||||
} else if (env.img_url && typeof env.img_url.get === 'function') {
|
||||
// 回退到KV存储
|
||||
return new KVAdapter(env.img_url);
|
||||
} else {
|
||||
console.error('No database configured. Please configure either D1 (env.img_d1) or KV (env.img_url)');
|
||||
console.error('No database configured. Please configure either KV (env.img_url) or D1 (env.img_d1).');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export async function checkDatabaseConfig(context) {
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "数据库未配置 / Database not configured",
|
||||
message: "请配置 D1 数据库 (env.img_d1) 或 KV 存储 (env.img_url)。 / Please configure D1 database (env.img_d1) or KV storage (env.img_url)."
|
||||
message: "请配置 KV 存储 (env.img_url) 或 D1 数据库 (env.img_d1)。 / Please configure KV storage (env.img_url) or D1 database (env.img_d1)."
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
|
||||
@@ -21,9 +21,10 @@ export async function userAuthCheck(env, url, request, requiredPermission = null
|
||||
const securityConfig = await fetchSecurityConfig(env);
|
||||
const rightAuthCode = securityConfig.auth.user.authCode;
|
||||
|
||||
// 优先从请求 URL 获取 authCode
|
||||
// 优先从请求 URL 参数获取 authCode
|
||||
let authCode = url.searchParams.get('authCode');
|
||||
// 如果 URL 中没有 authCode,从 Referer 中获取
|
||||
|
||||
// 如果 URL 参数中没有 authCode,从 Referer 中获取
|
||||
if (!authCode) {
|
||||
const referer = request.headers.get('Referer');
|
||||
if (referer) {
|
||||
@@ -35,10 +36,12 @@ export async function userAuthCheck(env, url, request, requiredPermission = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 Referer 中没有 authCode,从请求头中获取
|
||||
if (!authCode) {
|
||||
authCode = request.headers.get('authCode');
|
||||
}
|
||||
|
||||
// 如果请求头中没有 authCode,从 Cookie 中获取
|
||||
if (!authCode) {
|
||||
const cookies = request.headers.get('Cookie');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><link rel="apple-touch-icon" href="/logo.png"><link rel="mask-icon" href="/logo.png" color="#f4b400"><meta name="description" content="Sanyue ImgHub - A modern file hosting platform"><meta name="keywords" content="Sanyue, ImgHub, file hosting, image hosting, cloud storage"><meta name="author" content="SanyueQi"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.fe3722ff.js"></script><link href="/css/app.ed56be35.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html><style>/* 下拉菜单样式 */
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><link rel="apple-touch-icon" href="/logo.png"><link rel="mask-icon" href="/logo.png" color="#f4b400"><meta name="description" content="Sanyue ImgHub - A modern file hosting platform"><meta name="keywords" content="Sanyue, ImgHub, file hosting, image hosting, cloud storage"><meta name="author" content="SanyueQi"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.42f0e844.js"></script><link href="/css/app.6e9711cc.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html><style>/* 下拉菜单样式 */
|
||||
.el-dropdown__popper.el-popper {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
|
||||
BIN
index.html.gz
BIN
index.html.gz
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
1
js/162.8a5db2a3.js.map
Normal file
1
js/162.8a5db2a3.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
js/162.8a5db2a3.js.map.gz
Normal file
BIN
js/162.8a5db2a3.js.map.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/226.4c3e9291.js.gz
Normal file
BIN
js/226.4c3e9291.js.gz
Normal file
Binary file not shown.
1
js/226.4c3e9291.js.map
Normal file
1
js/226.4c3e9291.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
js/226.4c3e9291.js.map.gz
Normal file
BIN
js/226.4c3e9291.js.map.gz
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
1
js/259.eab0b4ae.js.map
Normal file
1
js/259.eab0b4ae.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
js/259.eab0b4ae.js.map.gz
Normal file
BIN
js/259.eab0b4ae.js.map.gz
Normal file
Binary file not shown.
2
js/442.a40d3211.js
Normal file
2
js/442.a40d3211.js
Normal file
File diff suppressed because one or more lines are too long
BIN
js/442.a40d3211.js.gz
Normal file
BIN
js/442.a40d3211.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/442.a40d3211.js.map.gz
Normal file
BIN
js/442.a40d3211.js.map.gz
Normal file
Binary file not shown.
2
js/459.3f268506.js
Normal file
2
js/459.3f268506.js
Normal file
File diff suppressed because one or more lines are too long
BIN
js/459.3f268506.js.gz
Normal file
BIN
js/459.3f268506.js.gz
Normal file
Binary file not shown.
1
js/459.3f268506.js.map
Normal file
1
js/459.3f268506.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
js/459.3f268506.js.map.gz
Normal file
BIN
js/459.3f268506.js.map.gz
Normal file
Binary file not shown.
2
js/499.d357974a.js
Normal file
2
js/499.d357974a.js
Normal file
File diff suppressed because one or more lines are too long
BIN
js/499.d357974a.js.gz
Normal file
BIN
js/499.d357974a.js.gz
Normal file
Binary file not shown.
1
js/499.d357974a.js.map
Normal file
1
js/499.d357974a.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
js/499.d357974a.js.map.gz
Normal file
BIN
js/499.d357974a.js.map.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[548],{4452:function(e,t,s){s.d(t,{A:function(){return v}});var n=s(6975),a=s(47),i=(s(5331),s(9648),s(9623)),l=(s(9092),s(4632)),o=s(3525),r=s(6768),d=s(4232),u=s(5130);const c={class:"login"},h={class:"login-container"},p={class:"login-title",tabindex:"0"},m={class:"input-wrapper"};function f(e,t,s,f,b,g){const y=o.A,k=l.A,w=i.WK,v=a.S2,L=n.A;return(0,r.uX)(),(0,r.CE)("div",c,[(0,r.bF)(y,{class:"toggle-dark"}),(0,r.bF)(k),(0,r.Lk)("div",h,[(0,r.Lk)("h1",p,(0,d.v_)(s.title),1),((0,r.uX)(!0),(0,r.CE)(r.FK,null,(0,r.pI)(s.fields,(e,s)=>((0,r.uX)(),(0,r.CE)("div",{key:e.key,class:"input-container"},[(0,r.Lk)("label",{class:"input-name",ref_for:!0,ref:`inputLabel${s}`,style:(0,d.Tr)({"--underline-width":b.labelUnderlineWidths[s]+"px"})},(0,d.v_)(e.label),5),(0,r.Lk)("div",m,[(0,r.bF)(w,{modelValue:b.formData[e.key],"onUpdate:modelValue":t=>b.formData[e.key]=t,placeholder:e.placeholder,type:e.type||"text","show-password":e.showPassword,class:"password-input",onKeyup:(0,u.jR)(g.handleSubmit,["enter","native"]),onFocus:g.handleInputFocus,onBlur:g.handleInputBlur},null,8,["modelValue","onUpdate:modelValue","placeholder","type","show-password","onKeyup","onFocus","onBlur"]),t[0]||(t[0]=(0,r.Lk)("div",{class:"input-underline"},null,-1))])]))),128)),(0,r.bF)(v,{class:"submit",type:"primary",onClick:g.handleSubmit},{default:(0,r.k6)(()=>[(0,r.eW)((0,d.v_)(s.submitText),1)]),_:1},8,["onClick"])]),(0,r.bF)(L,{class:"footer"})])}s(8111),s(7588);var b=s(782),g=s(8903),y={name:"BaseLogin",mixins:[g.A],props:{title:{type:String,required:!0},fields:{type:Array,required:!0},submitText:{type:String,default:"登录"},backgroundKey:{type:String,required:!0},isAdmin:{type:Boolean,default:!1}},data(){return{formData:{},labelUnderlineWidths:[]}},computed:{...(0,b.L8)(["userConfig"])},watch:{fields:{handler(){this.$nextTick(()=>{this.calculateLabelWidths()})},deep:!0}},components:{Footer:n.A,ToggleDark:o.A,Logo:l.A},mounted(){this.initFormData(),this.initializeBackground(this.backgroundKey,".login",!this.isAdmin,!0),this.$nextTick(()=>{this.calculateLabelWidths()})},methods:{initFormData(){const e={};this.fields.forEach(t=>{e[t.key]=""}),this.formData=e,this.labelUnderlineWidths=new Array(this.fields.length).fill(0)},calculateLabelWidths(){this.$nextTick(()=>{this.fields.forEach((e,t)=>{const s=this.$refs[`inputLabel${t}`];if(s&&s[0]){const n=document.createElement("canvas"),a=n.getContext("2d"),i=s[0],l=window.getComputedStyle(i);a.font=`${l.fontWeight} ${l.fontSize} ${l.fontFamily}`;const o=a.measureText(e.label).width;this.labelUnderlineWidths[t]=Math.ceil(o)+3}})})},handleSubmit(){this.$emit("submit",{...this.formData})},handleInputFocus(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.add("focused")}},handleInputBlur(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.remove("focused")}}}},k=s(1241);const w=(0,k.A)(y,[["render",f],["__scopeId","data-v-61197d6f"]]);var v=w},8351:function(e,t,s){s.r(t),s.d(t,{default:function(){return u}});var n=s(4452),a=s(6768);function i(e,t,s,i,l,o){const r=n.A;return(0,a.uX)(),(0,a.Wv)(r,{title:"管理端登录",fields:l.loginFields,"submit-text":"登录","background-key":"adminLoginBkImg","is-admin":!0,onSubmit:o.handleLogin},null,8,["fields","onSubmit"])}s(4114),s(4979);var l=s(9189),o={data(){return{loginFields:[{key:"username",label:"用户名",placeholder:"请输入用户名",type:"text"},{key:"password",label:"密码",placeholder:"请输入密码",type:"password",showPassword:!0}]}},components:{BaseLogin:n.A},methods:{async handleLogin(e){const{username:t,password:s}=e,n=btoa(`${t}:${s}`);try{const e=await l.A.get("/api/manage/check",{headers:{Authorization:`Basic ${n}`},withCredentials:!0});200===e.status&&(this.$store.commit("setCredentials",n),this.$router.push("/dashboard"))}catch(a){a.response&&401===a.response.status?this.$message.error("用户名或密码错误"):this.$message.error("服务器错误")}}}},r=s(1241);const d=(0,r.A)(o,[["render",i]]);var u=d}}]);
|
||||
//# sourceMappingURL=548.a8fc5a24.js.map
|
||||
//# sourceMappingURL=548.6255b5bc.js.map
|
||||
BIN
js/548.6255b5bc.js.gz
Normal file
BIN
js/548.6255b5bc.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/548.6255b5bc.js.map.gz
Normal file
BIN
js/548.6255b5bc.js.map.gz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[585],{4452:function(e,t,n){n.d(t,{A:function(){return v}});var s=n(6975),i=n(47),a=(n(5331),n(9648),n(9623)),o=(n(9092),n(4632)),l=n(3525),r=n(6768),u=n(4232),d=n(5130);const c={class:"login"},h={class:"login-container"},p={class:"login-title",tabindex:"0"},m={class:"input-wrapper"};function f(e,t,n,f,g,b){const y=l.A,k=o.A,w=a.WK,v=i.S2,L=s.A;return(0,r.uX)(),(0,r.CE)("div",c,[(0,r.bF)(y,{class:"toggle-dark"}),(0,r.bF)(k),(0,r.Lk)("div",h,[(0,r.Lk)("h1",p,(0,u.v_)(n.title),1),((0,r.uX)(!0),(0,r.CE)(r.FK,null,(0,r.pI)(n.fields,(e,n)=>((0,r.uX)(),(0,r.CE)("div",{key:e.key,class:"input-container"},[(0,r.Lk)("label",{class:"input-name",ref_for:!0,ref:`inputLabel${n}`,style:(0,u.Tr)({"--underline-width":g.labelUnderlineWidths[n]+"px"})},(0,u.v_)(e.label),5),(0,r.Lk)("div",m,[(0,r.bF)(w,{modelValue:g.formData[e.key],"onUpdate:modelValue":t=>g.formData[e.key]=t,placeholder:e.placeholder,type:e.type||"text","show-password":e.showPassword,class:"password-input",onKeyup:(0,d.jR)(b.handleSubmit,["enter","native"]),onFocus:b.handleInputFocus,onBlur:b.handleInputBlur},null,8,["modelValue","onUpdate:modelValue","placeholder","type","show-password","onKeyup","onFocus","onBlur"]),t[0]||(t[0]=(0,r.Lk)("div",{class:"input-underline"},null,-1))])]))),128)),(0,r.bF)(v,{class:"submit",type:"primary",onClick:b.handleSubmit},{default:(0,r.k6)(()=>[(0,r.eW)((0,u.v_)(n.submitText),1)]),_:1},8,["onClick"])]),(0,r.bF)(L,{class:"footer"})])}n(8111),n(7588);var g=n(782),b=n(8903),y={name:"BaseLogin",mixins:[b.A],props:{title:{type:String,required:!0},fields:{type:Array,required:!0},submitText:{type:String,default:"登录"},backgroundKey:{type:String,required:!0},isAdmin:{type:Boolean,default:!1}},data(){return{formData:{},labelUnderlineWidths:[]}},computed:{...(0,g.L8)(["userConfig"])},watch:{fields:{handler(){this.$nextTick(()=>{this.calculateLabelWidths()})},deep:!0}},components:{Footer:s.A,ToggleDark:l.A,Logo:o.A},mounted(){this.initFormData(),this.initializeBackground(this.backgroundKey,".login",!this.isAdmin,!0),this.$nextTick(()=>{this.calculateLabelWidths()})},methods:{initFormData(){const e={};this.fields.forEach(t=>{e[t.key]=""}),this.formData=e,this.labelUnderlineWidths=new Array(this.fields.length).fill(0)},calculateLabelWidths(){this.$nextTick(()=>{this.fields.forEach((e,t)=>{const n=this.$refs[`inputLabel${t}`];if(n&&n[0]){const s=document.createElement("canvas"),i=s.getContext("2d"),a=n[0],o=window.getComputedStyle(a);i.font=`${o.fontWeight} ${o.fontSize} ${o.fontFamily}`;const l=i.measureText(e.label).width;this.labelUnderlineWidths[t]=Math.ceil(l)+3}})})},handleSubmit(){this.$emit("submit",{...this.formData})},handleInputFocus(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.add("focused")}},handleInputBlur(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.remove("focused")}}}},k=n(1241);const w=(0,k.A)(y,[["render",f],["__scopeId","data-v-61197d6f"]]);var v=w},9206:function(e,t,n){n.r(t),n.d(t,{default:function(){return p}});var s=n(4452),i=n(6768);function a(e,t,n,a,o,l){const r=s.A;return(0,i.uX)(),(0,i.Wv)(r,{title:l.loginTitle,fields:o.loginFields,"submit-text":"登录","background-key":"loginBkImg","is-admin":!1,onSubmit:l.handleLogin},null,8,["title","fields","onSubmit"])}n(4114);var o=n(4570),l=n.n(o),r=n(9189),u=n(782),d={data(){return{loginFields:[{key:"password",label:"密码",placeholder:"请输入认证码",type:"password",showPassword:!0}]}},computed:{...(0,u.L8)(["userConfig"]),ownerName(){return this.userConfig?.ownerName||"Sanyue"},loginTitle(){return`登录到 ${this.ownerName} 图床`}},components:{BaseLogin:s.A},methods:{handleLogin(e){const{password:t}=e,n=""===t?"unset":t;r.A.post("/api/login",{authCode:t}).then(e=>{200===e.status?(l().set("authCode",n,"14d"),this.$router.push("/"),this.$message.success("登录成功")):this.$message.error("登录失败,请检查密码是否正确")}).catch(e=>{this.$message.error("登录失败,请检查密码是否正确")})}}},c=n(1241);const h=(0,c.A)(d,[["render",a]]);var p=h}}]);
|
||||
//# sourceMappingURL=585.41725b71.js.map
|
||||
//# sourceMappingURL=585.c628b4e4.js.map
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/585.c628b4e4.js.map.gz
Normal file
BIN
js/585.c628b4e4.js.map.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/845.1093f728.js.gz
Normal file
BIN
js/845.1093f728.js.gz
Normal file
Binary file not shown.
1
js/845.1093f728.js.map
Normal file
1
js/845.1093f728.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
js/845.1093f728.js.map.gz
Normal file
BIN
js/845.1093f728.js.map.gz
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/917.d81a041b.js.gz
Normal file
BIN
js/917.d81a041b.js.gz
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user