145 lines
5.3 KiB
Dart
145 lines
5.3 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../../errors/server_exception.dart';
|
|
import '../nextcloud_ocs.dart';
|
|
import 'file_sharing_api_params.dart';
|
|
import 'queries/share/share.dart';
|
|
import 'queries/share/share_update_params.dart';
|
|
|
|
/// OCS `files_sharing` API (Nextcloud Sharing API v1). Per the official docs:
|
|
/// list/delete pass parameters via the URL (query / path), create and update
|
|
/// pass them in the request **body** (form-urlencoded), and every call adds
|
|
/// `format=json` so the server replies with JSON instead of XML.
|
|
///
|
|
/// All calls surface failures as [ServerException] so they map to friendly
|
|
/// messages via `errorToUserMessage`.
|
|
class FileSharingApi {
|
|
static const String _base = 'apps/files_sharing/api/v1/shares';
|
|
|
|
/// Creates a share. Returns the created [Share] (callers that don't need it —
|
|
/// e.g. Talk file sharing — can ignore the result).
|
|
Future<Share> share(FileSharingApiParams query) async {
|
|
final endpoint = NextcloudOcs.uri(
|
|
_base,
|
|
queryParameters: {'format': 'json'},
|
|
);
|
|
final response = await http.post(
|
|
endpoint,
|
|
headers: NextcloudOcs.headers(),
|
|
body: _stringForm(query.toJson()),
|
|
);
|
|
return _decodeShare(response, action: 'Freigabe erstellen');
|
|
}
|
|
|
|
/// Lists shares for the given OCS [path] (see `ocs_path.dart`). [reshares]
|
|
/// includes shares the current user received and re-shared.
|
|
Future<List<Share>> listForPath(String path, {bool reshares = false}) async {
|
|
final endpoint = NextcloudOcs.uri(
|
|
_base,
|
|
queryParameters: {
|
|
'format': 'json',
|
|
'path': path,
|
|
'reshares': reshares.toString(),
|
|
'subfiles': 'false',
|
|
},
|
|
);
|
|
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
|
final data = _decodeData(response, action: 'Freigaben laden');
|
|
if (data is! List) return const [];
|
|
return data
|
|
.whereType<Map<String, dynamic>>()
|
|
.map(Share.fromJson)
|
|
.toList(growable: false);
|
|
}
|
|
|
|
/// Updates an existing share. Returns the updated [Share].
|
|
Future<Share> update(int shareId, ShareUpdateParams params) async {
|
|
final endpoint = NextcloudOcs.uri(
|
|
'$_base/$shareId',
|
|
queryParameters: {'format': 'json'},
|
|
);
|
|
final response = await http.put(
|
|
endpoint,
|
|
headers: NextcloudOcs.headers(),
|
|
body: params.toQuery(),
|
|
);
|
|
return _decodeShare(response, action: 'Freigabe ändern');
|
|
}
|
|
|
|
/// Deletes (revokes) a share.
|
|
Future<void> remove(int shareId) async {
|
|
final endpoint = NextcloudOcs.uri(
|
|
'$_base/$shareId',
|
|
queryParameters: {'format': 'json'},
|
|
);
|
|
final response = await http.delete(
|
|
endpoint,
|
|
headers: NextcloudOcs.headers(),
|
|
);
|
|
_decodeData(response, action: 'Freigabe löschen');
|
|
}
|
|
|
|
/// Stringifies a json map into form fields (the OCS body is
|
|
/// `application/x-www-form-urlencoded`). Null values are already dropped by
|
|
/// the params' `includeIfNull: false`.
|
|
Map<String, String> _stringForm(Map<String, dynamic> json) =>
|
|
json.map((key, value) => MapEntry(key, value.toString()));
|
|
|
|
/// Decodes a single-share response (create/update). Throws if the payload is
|
|
/// not a share object.
|
|
Share _decodeShare(http.Response response, {required String action}) {
|
|
final data = _decodeData(response, action: action);
|
|
if (data is! Map<String, dynamic>) {
|
|
throw ServerException(
|
|
statusCode: response.statusCode,
|
|
technicalDetails: 'Unerwartete Antwort für "$action": ${response.body}',
|
|
);
|
|
}
|
|
return Share.fromJson(data);
|
|
}
|
|
|
|
/// Validates the HTTP/OCS envelope and returns the `ocs.data` payload, or
|
|
/// throws a [ServerException] carrying the server's OCS message when present.
|
|
///
|
|
/// Every access is type-checked rather than cast: PHP's `json_encode` turns
|
|
/// an empty associative array into a JSON array (`[]`), and `statuscode` can
|
|
/// arrive as a string — a hard `as` cast on either would throw a raw
|
|
/// TypeError that surfaces as the generic "something went wrong" message.
|
|
Object? _decodeData(http.Response response, {required String action}) {
|
|
dynamic decoded;
|
|
try {
|
|
decoded = jsonDecode(response.body);
|
|
} catch (_) {
|
|
decoded = null;
|
|
}
|
|
final ocs = decoded is Map<String, dynamic> ? decoded['ocs'] : null;
|
|
final ocsMap = ocs is Map<String, dynamic> ? ocs : null;
|
|
final meta = ocsMap?['meta'];
|
|
final metaMap = meta is Map<String, dynamic> ? meta : null;
|
|
|
|
final rawStatus = metaMap?['statuscode'];
|
|
final ocsStatus = rawStatus is int
|
|
? rawStatus
|
|
: (rawStatus is String ? int.tryParse(rawStatus) : null);
|
|
final rawMessage = metaMap?['message'];
|
|
final ocsMessage = rawMessage is String ? rawMessage : null;
|
|
|
|
// OCS v2 mirrors the HTTP status; accept any 2xx. Success OCS statuscodes
|
|
// are 100 (v1 carry-over) or 200.
|
|
final httpOk = response.statusCode >= 200 && response.statusCode < 300;
|
|
final ocsOk = ocsStatus == null || ocsStatus == 100 || ocsStatus == 200;
|
|
if (!httpOk || !ocsOk) {
|
|
throw ServerException(
|
|
statusCode: response.statusCode,
|
|
userMessage: ocsMessage != null && ocsMessage.isNotEmpty
|
|
? '$action fehlgeschlagen: $ocsMessage'
|
|
: null,
|
|
technicalDetails: '$action: ${response.statusCode} ${response.body}',
|
|
);
|
|
}
|
|
return ocsMap?['data'];
|
|
}
|
|
}
|