mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
237 lines
7.9 KiB
Dart
237 lines
7.9 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:logging/logging.dart';
|
|
import 'package:nc_photos/entity/file.dart';
|
|
import 'package:nc_photos/string_extension.dart';
|
|
import 'package:xml/xml.dart';
|
|
|
|
class WebdavFileParser {
|
|
List<File> call(XmlDocument xml) {
|
|
_namespaces = _parseNamespaces(xml);
|
|
final body = () {
|
|
try {
|
|
return xml.children.whereType<XmlElement>().firstWhere((element) =>
|
|
element.matchQualifiedName("multistatus",
|
|
prefix: "DAV:", namespaces: _namespaces));
|
|
} catch (_) {
|
|
_log.severe("[call] Missing element: multistatus");
|
|
rethrow;
|
|
}
|
|
}();
|
|
return body.children
|
|
.whereType<XmlElement>()
|
|
.where((element) => element.matchQualifiedName("response",
|
|
prefix: "DAV:", namespaces: _namespaces))
|
|
.map((element) {
|
|
try {
|
|
return _toFile(element);
|
|
} catch (e, stacktrace) {
|
|
_log.severe("[call] Failed parsing XML", e, stacktrace);
|
|
return null;
|
|
}
|
|
})
|
|
.where((element) => element != null)
|
|
.toList();
|
|
}
|
|
|
|
Map<String, String> get namespaces => _namespaces;
|
|
|
|
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;
|
|
Metadata metadata;
|
|
|
|
for (final child in element.children.whereType<XmlElement>()) {
|
|
if (child.matchQualifiedName("href",
|
|
prefix: "DAV:", namespaces: _namespaces)) {
|
|
path = Uri.decodeComponent(child.innerText).trimLeftAny("/");
|
|
} 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 =
|
|
_PropParser(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;
|
|
metadata = propParser.metadata;
|
|
}
|
|
}
|
|
|
|
return File(
|
|
path: path,
|
|
contentLength: contentLength,
|
|
contentType: contentType,
|
|
etag: etag,
|
|
lastModified: lastModified,
|
|
isCollection: isCollection,
|
|
usedBytes: usedBytes,
|
|
hasPreview: hasPreview,
|
|
metadata: metadata,
|
|
);
|
|
}
|
|
|
|
MapEntry<XmlName, dynamic> _xmlElementToMapEntry(XmlElement element) {
|
|
final key = element.name;
|
|
try {
|
|
final textNode = element.children
|
|
.where((node) => node is XmlText && node.text.trim().isNotEmpty)
|
|
.first;
|
|
return MapEntry(key, textNode.text);
|
|
} on StateError {
|
|
// No text
|
|
final value = <XmlName, dynamic>{};
|
|
for (final e in element.children.whereType<XmlElement>()) {
|
|
final entry = _xmlElementToMapEntry(e);
|
|
value[entry.key] = entry.value;
|
|
}
|
|
return MapEntry(key, value);
|
|
}
|
|
}
|
|
|
|
var _namespaces = <String, String>{};
|
|
|
|
static final _log =
|
|
Logger("entity.webdav_response_parser.WebdavResponseParser");
|
|
}
|
|
|
|
class _PropParser {
|
|
_PropParser({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";
|
|
}
|
|
}
|
|
// 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;
|
|
Metadata get metadata => _metadata;
|
|
|
|
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;
|
|
Metadata _metadata;
|
|
}
|
|
|
|
extension on XmlElement {
|
|
bool matchQualifiedName(
|
|
String local, {
|
|
String prefix,
|
|
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));
|
|
}
|
|
}
|