implemented a comprehensive Nextcloud file sharing system with support for user, group, and public link shares with gating based on server-side permissions; added sharing management interfaces including a share sheet; updated the file list with visual badges for incoming shares and improved OCS API response handling.

This commit is contained in:
2026-06-02 21:42:08 +02:00
parent b6d06dd3b4
commit baa26a6e79
33 changed files with 2453 additions and 29 deletions
@@ -1,21 +1,144 @@
import 'dart:io';
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 {
Future<void> share(FileSharingApiParams query) async {
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(
'apps/files_sharing/api/v1/shares',
queryParameters: query.toJson(),
_base,
queryParameters: {'format': 'json'},
);
final response = await http.post(endpoint, headers: NextcloudOcs.headers());
if (response.statusCode != HttpStatus.ok) {
throw Exception(
'Api call failed with ${response.statusCode}: ${response.body}',
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'];
}
}