mirror of
https://github.com/MarSeventh/CloudFlare-ImgBed.git
synced 2026-04-24 22:25:07 +00:00
105
README.md
105
README.md
@@ -116,7 +116,110 @@
|
||||
</details>
|
||||
|
||||
|
||||
# 4. Tips
|
||||
# 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
|
||||
|
||||
- **前端开源**:参见[MarSeventh/Sanyue-ImgHub](https://github.com/MarSeventh/Sanyue-ImgHub)项目。
|
||||
|
||||
|
||||
105
README_en.md
105
README_en.md
@@ -102,7 +102,110 @@ Provides detailed deployment documentation, feature docs, development plans, upd
|
||||
|
||||
</details>
|
||||
|
||||
# 4. Tips
|
||||
# 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
|
||||
|
||||
- Frontend is open source, see [MarSeventh/Sanyue-ImgHub](https://github.com/MarSeventh/Sanyue-ImgHub).
|
||||
|
||||
|
||||
262
WebDAV-API Bridge Cloudflare Worker.js
Normal file
262
WebDAV-API Bridge Cloudflare Worker.js
Normal file
@@ -0,0 +1,262 @@
|
||||
// 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>`;
|
||||
}
|
||||
Reference in New Issue
Block a user