import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; /// 시험용 작은 WebDAV 서버 (실제 HTTP): [root] 폴더를 /dav/ 아래로 내보낸다. 기본 인증 (user / pass). /// PROPFIND (Depth 0 · 1) · GET · PUT · MKCOL · DELETE · MOVE · COPY · quota. class TestDavServer { final Directory root; final String user, pass; late HttpServer _server; final requests = []; /// X-plore 비교 5-1 ② (10/21): GET 의 Range 를 따를지 (처음 false - 예전 시험은 Range 를 모르는 서버로) · 받은 Range 머리 bool ranges = false; final rangeHeaders = []; TestDavServer(this.root, {this.user = 'user', this.pass = 'pass'}); String get url => 'http://127.0.0.1:${_server.port}/dav'; Future start() async { _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); _server.listen(_handle); } Future stop() => _server.close(force: true); String _local(String uriPath) { var rel = Uri.decodeFull(uriPath); if (rel.startsWith('/dav')) rel = rel.substring(4); rel = rel.replaceAll(RegExp(r'^/+|/+$'), ''); return rel.isEmpty ? root.path : p.join(root.path, p.joinAll(rel.split('/'))); } String _href(String local, bool dir) { final rel = p.relative(local, from: root.path).replaceAll('\\', '/'); final parts = rel == '.' ? [] : rel.split('/'); return '/dav/${parts.map(Uri.encodeComponent).join('/')}${dir && parts.isNotEmpty ? '/' : ''}'; } String _entry(FileSystemEntity e) { final dir = e is Directory; final st = e.statSync(); return '${_href(e.path, dir)}' '${dir ? '' : ''}' '${dir ? '' : '${st.size}'}' '${HttpDate.format(st.modified)}' '1000000500' 'HTTP/1.1 200 OK'; } Future _handle(HttpRequest req) async { final res = req.response; requests.add('${req.method} ${req.uri.path}'); final auth = req.headers.value(HttpHeaders.authorizationHeader); if (auth != 'Basic ${base64Encode(utf8.encode('$user:$pass'))}') { res.statusCode = 401; await res.close(); return; } final local = _local(req.uri.path); final type = FileSystemEntity.typeSync(local); try { switch (req.method) { case 'PROPFIND': await req.drain(); if (type == FileSystemEntityType.notFound) { res.statusCode = 404; break; } final depth = req.headers.value('Depth') ?? '1'; final items = [type == FileSystemEntityType.directory ? Directory(local) : File(local)]; if (depth != '0' && type == FileSystemEntityType.directory) items.addAll(Directory(local).listSync()); res.statusCode = 207; res.headers.contentType = ContentType('application', 'xml', charset: 'utf-8'); res.write('${items.map(_entry).join()}'); case 'GET': if (type != FileSystemEntityType.file) { res.statusCode = 404; break; } final range = req.headers.value(HttpHeaders.rangeHeader); if (range != null) rangeHeaders.add(range); final m = range == null ? null : RegExp(r'^bytes=(\d+)-(\d*)$').firstMatch(range); if (ranges && m != null) { final len = File(local).lengthSync(); final start = int.parse(m[1]!); final end = m[2]!.isEmpty ? len - 1 : int.parse(m[2]!).clamp(0, len - 1); res.statusCode = 206; res.headers.set(HttpHeaders.contentRangeHeader, 'bytes $start-$end/$len'); res.contentLength = end - start + 1; await res.addStream(File(local).openRead(start, end + 1)); break; } res.contentLength = File(local).lengthSync(); await res.addStream(File(local).openRead()); case 'PUT': if (!Directory(p.dirname(local)).existsSync()) { await req.drain(); res.statusCode = 409; break; } final out = File(local).openWrite(); await out.addStream(req); await out.close(); res.statusCode = 201; case 'MKCOL': await req.drain(); if (type != FileSystemEntityType.notFound) { res.statusCode = 405; } else if (!Directory(p.dirname(local)).existsSync()) { res.statusCode = 409; } else { Directory(local).createSync(); res.statusCode = 201; } case 'DELETE': await req.drain(); if (type == FileSystemEntityType.directory) { Directory(local).deleteSync(recursive: true); } else if (type == FileSystemEntityType.file) { File(local).deleteSync(); } else { res.statusCode = 404; break; } res.statusCode = 204; case 'MOVE' || 'COPY': await req.drain(); final dest = _local(Uri.parse(req.headers.value('Destination')!).path); if (FileSystemEntity.typeSync(dest) != FileSystemEntityType.notFound && req.headers.value('Overwrite') == 'F') { res.statusCode = 412; break; } if (req.method == 'MOVE') { if (type == FileSystemEntityType.directory) { Directory(local).renameSync(dest); } else { File(local).renameSync(dest); } } else { if (type == FileSystemEntityType.directory) { _copyDir(Directory(local), Directory(dest)); } else { File(local).copySync(dest); } } res.statusCode = 201; default: await req.drain(); res.statusCode = 405; } } catch (e) { res.statusCode = 500; res.write('$e'); } await res.close(); } static void _copyDir(Directory from, Directory to) { to.createSync(recursive: true); for (final e in from.listSync()) { final t = p.join(to.path, p.basename(e.path)); if (e is Directory) { _copyDir(e, Directory(t)); } else if (e is File) { e.copySync(t); } } } }