nc-photos/lib/entity/webdav_response_parser.dart
2022-02-08 18:22:55 +08:00

294 lines
10 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:xml/xml.dart';
class WebdavResponseParser {
List<File> parseFiles(XmlDocument xml) => _parse<File>(xml, _toFile);
Map<String, String> get namespaces => _namespaces;
List<T> _parse<T>(XmlDocument xml, T Function(XmlElement) mapper) {
_namespaces = _parseNamespaces(xml);
final body = () {
try {
return xml.children.whereType<XmlElement>().firstWhere((element) =>
element.matchQualifiedName("multistatus",
prefix: "DAV:", namespaces: _namespaces));
} catch (_) {
_log.shout("[_parse] Missing element: multistatus");
rethrow;
}
}();
return body.children
.whereType<XmlElement>()
.where((e) => e.matchQualifiedName("response",
prefix: "DAV:", namespaces: _namespaces))
.map((e) {
try {
return mapper(e);
} catch (e, stackTrace) {
_log.shout("[_parse] Failed parsing XML", e, stackTrace);
return null;
}
})
.whereType<T>()
.toList();
}
Map<String, String> _parseNamespaces(XmlDocument xml) {
final namespaces = <String, String>{};
final xmlContent = xml.descendants.whereType<XmlElement>().firstWhere(
(element) => !element.name.qualified.startsWith("?"),
orElse: () => XmlElement(XmlName.fromString("")));
for (final a in xmlContent.attributes) {
if (a.name.prefix == "xmlns") {
namespaces[a.name.local] = a.value;
} else if (a.name.local == "xmlns") {
namespaces["!"] = a.value;
}
}
// _log.fine("[_parseNamespaces] Namespaces: $namespaces");
return namespaces;
}
/// Map <DAV:response> contents to File
File _toFile(XmlElement element) {
String? path;
int? contentLength;
String? contentType;
String? etag;
DateTime? lastModified;
bool? isCollection;
int? usedBytes;
bool? hasPreview;
int? fileId;
CiString? ownerId;
Metadata? metadata;
bool? isArchived;
DateTime? overrideDateTime;
String? trashbinFilename;
String? trashbinOriginalLocation;
DateTime? trashbinDeletionTime;
for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("href",
prefix: "DAV:", namespaces: _namespaces)) {
path = _hrefToPath(child);
} else if (child.matchQualifiedName("propstat",
prefix: "DAV:", namespaces: _namespaces)) {
final status = child.children
.whereType<XmlElement>()
.firstWhere((element) => element.matchQualifiedName("status",
prefix: "DAV:", namespaces: _namespaces))
.innerText;
if (!status.contains(" 200 ")) {
continue;
}
final prop = child.children.whereType<XmlElement>().firstWhere(
(element) => element.matchQualifiedName("prop",
prefix: "DAV:", namespaces: _namespaces));
final propParser =
_FilePropParser(namespaces: _namespaces, logFilePath: path);
propParser.parse(prop);
contentLength = propParser.contentLength;
contentType = propParser.contentType;
etag = propParser.etag;
lastModified = propParser.lastModified;
isCollection = propParser.isCollection;
usedBytes = propParser.usedBytes;
hasPreview = propParser.hasPreview;
fileId = propParser.fileId;
ownerId = propParser.ownerId;
metadata = propParser.metadata;
isArchived = propParser.isArchived;
overrideDateTime = propParser.overrideDateTime;
trashbinFilename = propParser.trashbinFilename;
trashbinOriginalLocation = propParser.trashbinOriginalLocation;
trashbinDeletionTime = propParser.trashbinDeletionTime;
}
}
return File(
path: path!,
contentLength: contentLength,
contentType: contentType,
etag: etag,
lastModified: lastModified,
isCollection: isCollection,
usedBytes: usedBytes,
hasPreview: hasPreview,
fileId: fileId,
ownerId: ownerId,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
trashbinFilename: trashbinFilename,
trashbinOriginalLocation: trashbinOriginalLocation,
trashbinDeletionTime: trashbinDeletionTime,
);
}
String _hrefToPath(XmlElement href) {
final rawPath = Uri.decodeComponent(href.innerText).trimLeftAny("/");
final pos = rawPath.indexOf("remote.php");
if (pos == -1) {
// what?
_log.warning("[_hrefToPath] Unknown href value: $rawPath");
return rawPath;
} else {
return rawPath.substring(pos);
}
}
var _namespaces = <String, String>{};
static final _log =
Logger("entity.webdav_response_parser.WebdavResponseParser");
}
class _FilePropParser {
_FilePropParser({
this.namespaces = const {},
this.logFilePath,
});
/// Parse <DAV:prop> element contents
void parse(XmlElement element) {
for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("getlastmodified",
prefix: "DAV:", namespaces: namespaces)) {
_lastModified = HttpDate.parse(child.innerText);
} else if (child.matchQualifiedName("getcontentlength",
prefix: "DAV:", namespaces: namespaces)) {
_contentLength = int.parse(child.innerText);
} else if (child.matchQualifiedName("getcontenttype",
prefix: "DAV:", namespaces: namespaces)) {
_contentType = child.innerText;
} else if (child.matchQualifiedName("getetag",
prefix: "DAV:", namespaces: namespaces)) {
_etag = child.innerText.replaceAll("\"", "");
} else if (child.matchQualifiedName("quota-used-bytes",
prefix: "DAV:", namespaces: namespaces)) {
_usedBytes = int.parse(child.innerText);
} else if (child.matchQualifiedName("resourcetype",
prefix: "DAV:", namespaces: namespaces)) {
_isCollection = child.children.whereType<XmlElement>().any((element) =>
element.matchQualifiedName("collection",
prefix: "DAV:", namespaces: namespaces));
} else if (child.matchQualifiedName("has-preview",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_hasPreview = child.innerText == "true";
} else if (child.matchQualifiedName("fileid",
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
_fileId = int.parse(child.innerText);
} else if (child.matchQualifiedName("owner-id",
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
_ownerId = child.innerText.toCi();
} else if (child.matchQualifiedName("trashbin-filename",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_trashbinFilename = child.innerText;
} else if (child.matchQualifiedName("trashbin-original-location",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_trashbinOriginalLocation = child.innerText;
} else if (child.matchQualifiedName("trashbin-deletion-time",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_trashbinDeletionTime = DateTime.fromMillisecondsSinceEpoch(
int.parse(child.innerText) * 1000);
} else if (child.matchQualifiedName("is-archived",
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
_isArchived = child.innerText == "true";
} else if (child.matchQualifiedName("override-date-time",
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
_overrideDateTime = DateTime.parse(child.innerText);
}
}
// 2nd pass that depends on data in 1st pass
for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("metadata",
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
_metadata = Metadata.fromJson(
jsonDecode(child.innerText),
upgraderV1: MetadataUpgraderV1(
fileContentType: _contentType,
logFilePath: logFilePath,
),
upgraderV2: MetadataUpgraderV2(
fileContentType: _contentType,
logFilePath: logFilePath,
),
);
}
}
}
DateTime? get lastModified => _lastModified;
int? get contentLength => _contentLength;
String? get contentType => _contentType;
String? get etag => _etag;
int? get usedBytes => _usedBytes;
bool? get isCollection => _isCollection;
bool? get hasPreview => _hasPreview;
int? get fileId => _fileId;
CiString? get ownerId => _ownerId;
Metadata? get metadata => _metadata;
bool? get isArchived => _isArchived;
DateTime? get overrideDateTime => _overrideDateTime;
String? get trashbinFilename => _trashbinFilename;
String? get trashbinOriginalLocation => _trashbinOriginalLocation;
DateTime? get trashbinDeletionTime => _trashbinDeletionTime;
final Map<String, String> namespaces;
/// File path for logging only
final String? logFilePath;
DateTime? _lastModified;
int? _contentLength;
String? _contentType;
String? _etag;
int? _usedBytes;
bool? _isCollection;
bool? _hasPreview;
int? _fileId;
CiString? _ownerId;
Metadata? _metadata;
bool? _isArchived;
DateTime? _overrideDateTime;
String? _trashbinFilename;
String? _trashbinOriginalLocation;
DateTime? _trashbinDeletionTime;
}
extension on XmlElement {
bool matchQualifiedName(
String local, {
required String prefix,
required Map<String, String> namespaces,
}) {
final localNamespaces = <String, String>{};
for (final a in attributes) {
if (a.name.prefix == "xmlns") {
localNamespaces[a.name.local] = a.value;
} else if (a.name.local == "xmlns") {
localNamespaces["!"] = a.value;
}
}
return name.local == local &&
(name.prefix == prefix ||
// match default namespace
(name.prefix == null && namespaces["!"] == prefix) ||
// match global namespace
namespaces.entries
.where((element2) => element2.value == prefix)
.any((element) => element.key == name.prefix) ||
// match local namespace
localNamespaces.entries
.where((element2) => element2.value == prefix)
.any((element) => element.key == name.prefix));
}
}