feat(WebDAV): enhance listDirectory function for recursive listing and improve XML parsing
This commit is contained in:
parent
0786fd087e
commit
92a22fe253
|
|
@ -165,26 +165,72 @@ function toEntryInfo(e: WebDAVEntry, baseUrl: string): EntryInfo {
|
||||||
|
|
||||||
// ----- High-level operations -------------------------------------------
|
// ----- High-level operations -------------------------------------------
|
||||||
|
|
||||||
/** List the contents of a collection. Returns a sorted array of entries. */
|
/**
|
||||||
export async function listDirectory(path: string): Promise<EntryInfo[]> {
|
* List the contents of a collection. Returns a sorted array of entries.
|
||||||
|
* Pass `recursive: true` to request `Depth: infinity` from the server and
|
||||||
|
* return all descendants flattened — requires the server to support it
|
||||||
|
* (webdav3.py does after the depth-infinity fix).
|
||||||
|
*/
|
||||||
|
export async function listDirectory(path: string, recursive = false): Promise<EntryInfo[]> {
|
||||||
const manager = getWebDAVClient();
|
const manager = getWebDAVClient();
|
||||||
const base = manager.client.baseUrl;
|
const base = manager.client.baseUrl;
|
||||||
// The manager's `open` requires an absolute URL on the configured server.
|
|
||||||
const collectionUrl = pathToUrl(normalizePath(path), base);
|
const collectionUrl = pathToUrl(normalizePath(path), base);
|
||||||
const listing = await manager.open(collectionUrl);
|
|
||||||
const selfPath = normalizePath(path);
|
const selfPath = normalizePath(path);
|
||||||
|
|
||||||
|
// Parse the PROPFIND response directly rather than going through
|
||||||
|
// parsePropfindListing, which keys entries by display name and would
|
||||||
|
// silently drop any entries that share the same filename. Parsing
|
||||||
|
// the XML ourselves also avoids mutating the shared WebDAVManager
|
||||||
|
// navigation state (navToken, this.files).
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const depth: any = recursive ? 'infinity' : 1;
|
||||||
|
const doc = await manager.client.propfind(collectionUrl, PROPFIND_LIST_BODY, depth);
|
||||||
const entries: EntryInfo[] = [];
|
const entries: EntryInfo[] = [];
|
||||||
for (const e of Object.values(listing)) {
|
|
||||||
if (!e || !e.name) continue;
|
for (const node of Array.from(doc.querySelectorAll('response'))) {
|
||||||
const info = toEntryInfo(e, base);
|
const hrefEl = node.querySelector('href');
|
||||||
// Filter out the listing root itself (it appears as an entry whose
|
if (!hrefEl?.textContent) continue;
|
||||||
// path equals the requested collection) and any entries whose path
|
|
||||||
// doesn't sit directly under the requested collection.
|
// Find the first 200 propstat.
|
||||||
if (info.path === selfPath) continue;
|
let propsNode: Element | null = null;
|
||||||
const parent = splitPath(info.path).parent;
|
for (const propstat of Array.from(node.querySelectorAll('propstat'))) {
|
||||||
if (parent !== selfPath) continue;
|
const status = propstat.querySelector('status');
|
||||||
entries.push(info);
|
if (status && /200/.test(status.textContent ?? '')) {
|
||||||
|
propsNode = propstat;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!propsNode) continue;
|
||||||
|
|
||||||
|
const fullPath = pathFromUri(hrefEl.textContent, base);
|
||||||
|
|
||||||
|
// Skip the collection itself.
|
||||||
|
if (fullPath === selfPath) continue;
|
||||||
|
|
||||||
|
// Non-recursive: only include direct children.
|
||||||
|
if (!recursive && splitPath(fullPath).parent !== selfPath) continue;
|
||||||
|
|
||||||
|
const isDir = !!node.querySelector('resourcetype collection');
|
||||||
|
const displaynameEl = propsNode.querySelector('displayname');
|
||||||
|
const lengthEl = propsNode.querySelector('getcontentlength');
|
||||||
|
const mimeEl = propsNode.querySelector('getcontenttype');
|
||||||
|
const modifiedEl = propsNode.querySelector('getlastmodified');
|
||||||
|
|
||||||
|
// Prefer server displayname unless it looks like a full path.
|
||||||
|
const nameFromPath = basename(fullPath) || fullPath;
|
||||||
|
const dn = displaynameEl?.textContent ?? '';
|
||||||
|
const name = dn && !dn.includes('/') ? dn : nameFromPath;
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
name,
|
||||||
|
path: fullPath,
|
||||||
|
type: isDir ? 'folder' : 'file',
|
||||||
|
size: !isDir && lengthEl?.textContent ? parseInt(lengthEl.textContent, 10) : 0,
|
||||||
|
lastModified: modifiedEl?.textContent ? new Date(modifiedEl.textContent) : null,
|
||||||
|
contentType: !isDir && mimeEl?.textContent ? mimeEl.textContent : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Folders first, then alpha (case-insensitive).
|
// Folders first, then alpha (case-insensitive).
|
||||||
entries.sort((a, b) => {
|
entries.sort((a, b) => {
|
||||||
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
|
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
|
||||||
|
|
@ -334,9 +380,11 @@ export async function getFileRange(path: string, start: number, end: number): Pr
|
||||||
|
|
||||||
/** Convert a server-relative path to an absolute URL on the configured base. */
|
/** Convert a server-relative path to an absolute URL on the configured base. */
|
||||||
function pathToUrl(p: string, baseUrl: string): string {
|
function pathToUrl(p: string, baseUrl: string): string {
|
||||||
// baseUrl may already end with `/`; we just want "<base><path>".
|
// Encode each segment so spaces and special characters become valid URL
|
||||||
const sep = baseUrl.endsWith('/') || p.startsWith('/') ? '' : '/';
|
// components, while preserving the '/' separators.
|
||||||
return baseUrl + sep + p;
|
const encoded = p.split('/').map(seg => seg ? encodeURIComponent(seg) : '').join('/');
|
||||||
|
const sep = baseUrl.endsWith('/') || encoded.startsWith('/') ? '' : '/';
|
||||||
|
return baseUrl + sep + encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert a raw byte count to a short human string (e.g. "1.4 MB"). */
|
/** Convert a raw byte count to a short human string (e.g. "1.4 MB"). */
|
||||||
|
|
|
||||||
28
webdav3.py
28
webdav3.py
|
|
@ -710,13 +710,25 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
|
||||||
if (wp in props) == False:
|
if (wp in props) == False:
|
||||||
w.write(' <D:%s/>\n' % wp)
|
w.write(' <D:%s/>\n' % wp)
|
||||||
else:
|
else:
|
||||||
w.write(' <D:%s>%s</D:%s>\n' % (wp, str(props[wp]), wp))
|
val = str(props[wp])
|
||||||
|
if wp != 'resourcetype':
|
||||||
|
val = val.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
w.write(' <D:%s>%s</D:%s>\n' % (wp, val, wp))
|
||||||
w.write('</D:prop>\n<D:status>HTTP/1.1 200 OK</D:status>\n</D:propstat>\n</D:response>\n')
|
w.write('</D:prop>\n<D:status>HTTP/1.1 200 OK</D:status>\n</D:propstat>\n</D:response>\n')
|
||||||
|
|
||||||
|
def write_recursive(m):
|
||||||
|
write_props_member(w, m)
|
||||||
|
if m.type == Member.M_COLLECTION:
|
||||||
|
for child in m.getMembers():
|
||||||
|
write_recursive(child)
|
||||||
|
|
||||||
write_props_member(w, elem)
|
write_props_member(w, elem)
|
||||||
if depth == '1' and elem.type == Member.M_COLLECTION:
|
if depth == '1' and elem.type == Member.M_COLLECTION:
|
||||||
for m in elem.getMembers():
|
for m in elem.getMembers():
|
||||||
write_props_member(w, m)
|
write_props_member(w, m)
|
||||||
|
elif depth == 'infinity' and elem.type == Member.M_COLLECTION:
|
||||||
|
for m in elem.getMembers():
|
||||||
|
write_recursive(m)
|
||||||
w.write('</D:multistatus>')
|
w.write('</D:multistatus>')
|
||||||
self.send_header('Content-Length', str(w.getSize()))
|
self.send_header('Content-Length', str(w.getSize()))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
@ -967,19 +979,23 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
|
||||||
class BufWriter:
|
class BufWriter:
|
||||||
def __init__(self, w, debug=False):
|
def __init__(self, w, debug=False):
|
||||||
self.w = w
|
self.w = w
|
||||||
self.buf = ''
|
self.parts = []
|
||||||
|
self._size = 0
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
def write(self, s):
|
def write(self, s):
|
||||||
self.buf += s
|
b = s.encode('utf-8')
|
||||||
|
self.parts.append(b)
|
||||||
|
self._size += len(b)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
if self.debug: print('<- XML:', self.buf) # sys.stderr.write(self.buf)
|
data = b''.join(self.parts)
|
||||||
self.w.write(self.buf.encode('utf-8'))
|
if self.debug: print('<- XML:', data.decode('utf-8', errors='replace'))
|
||||||
|
self.w.write(data)
|
||||||
self.w.flush()
|
self.w.flush()
|
||||||
|
|
||||||
def getSize(self):
|
def getSize(self):
|
||||||
return len(self.buf.encode('utf-8'))
|
return self._size
|
||||||
|
|
||||||
class DAVServer(ThreadingMixIn, HTTPServer):
|
class DAVServer(ThreadingMixIn, HTTPServer):
|
||||||
def __init__(self, addr, handler, root, userpwd):
|
def __init__(self, addr, handler, root, userpwd):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user