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(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> 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(Share.fromJson) .toList(growable: false); } /// Updates an existing share. Returns the updated [Share]. Future 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 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 _stringForm(Map 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) { 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 ? decoded['ocs'] : null; final ocsMap = ocs is Map ? ocs : null; final meta = ocsMap?['meta']; final metaMap = meta is Map ? 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']; } }