feat: Enhance WebDAV file management and UI
- Add functionality to create new collections via API - Implement copy and move operations between collections - Improve image rendering in markdown preview with relative path resolution - Add support for previewing binary files (images, PDFs) - Refactor modal styling to use flat buttons and improve accessibility
This commit is contained in:
@@ -8,11 +8,11 @@ class WebDAVClient {
|
||||
this.baseUrl = baseUrl;
|
||||
this.currentCollection = null;
|
||||
}
|
||||
|
||||
|
||||
setCollection(collection) {
|
||||
this.currentCollection = collection;
|
||||
}
|
||||
|
||||
|
||||
getFullUrl(path) {
|
||||
if (!this.currentCollection) {
|
||||
throw new Error('No collection selected');
|
||||
@@ -20,7 +20,7 @@ class WebDAVClient {
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
return `${this.baseUrl}${this.currentCollection}/${cleanPath}`;
|
||||
}
|
||||
|
||||
|
||||
async getCollections() {
|
||||
const response = await fetch(this.baseUrl);
|
||||
if (!response.ok) {
|
||||
@@ -28,7 +28,25 @@ class WebDAVClient {
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
|
||||
async createCollection(collectionName) {
|
||||
// Use POST API to create collection (not MKCOL, as collections are managed by the server)
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name: collectionName })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errorData.error || `Failed to create collection: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async propfind(path = '', depth = '1') {
|
||||
const url = this.getFullUrl(path);
|
||||
const response = await fetch(url, {
|
||||
@@ -38,37 +56,64 @@ class WebDAVClient {
|
||||
'Content-Type': 'application/xml'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PROPFIND failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const xml = await response.text();
|
||||
return this.parseMultiStatus(xml);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* List files and directories in a path
|
||||
* Returns only direct children (depth=1) to avoid infinite recursion
|
||||
* @param {string} path - Path to list
|
||||
* @param {boolean} recursive - If true, returns all nested items (depth=infinity)
|
||||
* @returns {Promise<Array>} Array of items
|
||||
*/
|
||||
async list(path = '', recursive = false) {
|
||||
const depth = recursive ? 'infinity' : '1';
|
||||
const items = await this.propfind(path, depth);
|
||||
|
||||
// If not recursive, filter to only direct children
|
||||
if (!recursive && path) {
|
||||
// Normalize path (remove trailing slash)
|
||||
const normalizedPath = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
const pathDepth = normalizedPath.split('/').length;
|
||||
|
||||
// Filter items to only include direct children
|
||||
return items.filter(item => {
|
||||
const itemDepth = item.path.split('/').length;
|
||||
return itemDepth === pathDepth + 1;
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async get(path) {
|
||||
const url = this.getFullUrl(path);
|
||||
const response = await fetch(url);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
|
||||
async getBinary(path) {
|
||||
const url = this.getFullUrl(path);
|
||||
const response = await fetch(url);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return await response.blob();
|
||||
}
|
||||
|
||||
|
||||
async put(path, content) {
|
||||
const url = this.getFullUrl(path);
|
||||
const response = await fetch(url, {
|
||||
@@ -78,109 +123,144 @@ class WebDAVClient {
|
||||
},
|
||||
body: content
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PUT failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async putBinary(path, content) {
|
||||
const url = this.getFullUrl(path);
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: content
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PUT failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async delete(path) {
|
||||
const url = this.getFullUrl(path);
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DELETE failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async copy(sourcePath, destPath) {
|
||||
const sourceUrl = this.getFullUrl(sourcePath);
|
||||
const destUrl = this.getFullUrl(destPath);
|
||||
|
||||
|
||||
const response = await fetch(sourceUrl, {
|
||||
method: 'COPY',
|
||||
headers: {
|
||||
'Destination': destUrl
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`COPY failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async move(sourcePath, destPath) {
|
||||
const sourceUrl = this.getFullUrl(sourcePath);
|
||||
const destUrl = this.getFullUrl(destPath);
|
||||
|
||||
|
||||
const response = await fetch(sourceUrl, {
|
||||
method: 'MOVE',
|
||||
headers: {
|
||||
'Destination': destUrl
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`MOVE failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async mkcol(path) {
|
||||
const url = this.getFullUrl(path);
|
||||
const response = await fetch(url, {
|
||||
method: 'MKCOL'
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok && response.status !== 405) { // 405 means already exists
|
||||
throw new Error(`MKCOL failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Alias for mkcol
|
||||
async createFolder(path) {
|
||||
return await this.mkcol(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all parent directories exist for a given path
|
||||
* Creates missing parent directories recursively
|
||||
*/
|
||||
async ensureParentDirectories(filePath) {
|
||||
const parts = filePath.split('/');
|
||||
|
||||
// Remove the filename (last part)
|
||||
parts.pop();
|
||||
|
||||
// If no parent directories, nothing to do
|
||||
if (parts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create each parent directory level
|
||||
let currentPath = '';
|
||||
for (const part of parts) {
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
|
||||
try {
|
||||
await this.mkcol(currentPath);
|
||||
} catch (error) {
|
||||
// Ignore errors - directory might already exist
|
||||
// Only log for debugging
|
||||
console.debug(`Directory ${currentPath} might already exist:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async includeFile(path) {
|
||||
try {
|
||||
// Parse path: "collection:path/to/file" or "path/to/file"
|
||||
let targetCollection = this.currentCollection;
|
||||
let targetPath = path;
|
||||
|
||||
|
||||
if (path.includes(':')) {
|
||||
[targetCollection, targetPath] = path.split(':');
|
||||
}
|
||||
|
||||
|
||||
// Temporarily switch collection
|
||||
const originalCollection = this.currentCollection;
|
||||
this.currentCollection = targetCollection;
|
||||
|
||||
|
||||
const content = await this.get(targetPath);
|
||||
|
||||
|
||||
// Restore collection
|
||||
this.currentCollection = originalCollection;
|
||||
|
||||
|
||||
return content;
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot include file "${path}": ${error.message}`);
|
||||
@@ -191,32 +271,32 @@ class WebDAVClient {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, 'text/xml');
|
||||
const responses = doc.getElementsByTagNameNS('DAV:', 'response');
|
||||
|
||||
|
||||
const items = [];
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
const response = responses[i];
|
||||
const href = response.getElementsByTagNameNS('DAV:', 'href')[0].textContent;
|
||||
const propstat = response.getElementsByTagNameNS('DAV:', 'propstat')[0];
|
||||
const prop = propstat.getElementsByTagNameNS('DAV:', 'prop')[0];
|
||||
|
||||
|
||||
// Check if it's a collection (directory)
|
||||
const resourcetype = prop.getElementsByTagNameNS('DAV:', 'resourcetype')[0];
|
||||
const isDirectory = resourcetype.getElementsByTagNameNS('DAV:', 'collection').length > 0;
|
||||
|
||||
|
||||
// Get size
|
||||
const contentlengthEl = prop.getElementsByTagNameNS('DAV:', 'getcontentlength')[0];
|
||||
const size = contentlengthEl ? parseInt(contentlengthEl.textContent) : 0;
|
||||
|
||||
|
||||
// Extract path relative to collection
|
||||
const pathParts = href.split(`/${this.currentCollection}/`);
|
||||
const relativePath = pathParts.length > 1 ? pathParts[1] : '';
|
||||
|
||||
|
||||
// Skip the collection root itself
|
||||
if (!relativePath) continue;
|
||||
|
||||
|
||||
// Remove trailing slash from directories
|
||||
const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
|
||||
|
||||
|
||||
items.push({
|
||||
path: cleanPath,
|
||||
name: cleanPath.split('/').pop(),
|
||||
@@ -224,14 +304,14 @@ class WebDAVClient {
|
||||
size
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
buildTree(items) {
|
||||
const root = [];
|
||||
const map = {};
|
||||
|
||||
|
||||
// Sort items by path depth and name
|
||||
items.sort((a, b) => {
|
||||
const depthA = a.path.split('/').length;
|
||||
@@ -239,26 +319,26 @@ class WebDAVClient {
|
||||
if (depthA !== depthB) return depthA - depthB;
|
||||
return a.path.localeCompare(b.path);
|
||||
});
|
||||
|
||||
|
||||
items.forEach(item => {
|
||||
const parts = item.path.split('/');
|
||||
const parentPath = parts.slice(0, -1).join('/');
|
||||
|
||||
|
||||
const node = {
|
||||
...item,
|
||||
children: []
|
||||
};
|
||||
|
||||
|
||||
map[item.path] = node;
|
||||
|
||||
|
||||
if (parentPath && map[parentPath]) {
|
||||
map[parentPath].children.push(node);
|
||||
} else {
|
||||
root.push(node);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user