Support for enabling/disabling public urls

This commit is contained in:
Neeraj Gupta 2022-01-23 11:34:39 +05:30
parent acdae0e186
commit ff61b7113a
No known key found for this signature in database
GPG key ID: 3C5A1684DC1729E1
5 changed files with 204 additions and 1 deletions

View file

@ -26,6 +26,7 @@ class CollectionsDB {
static final columnPathDecryptionNonce = 'path_decryption_nonce';
static final columnVersion = 'version';
static final columnSharees = 'sharees';
static final columnPublicURLs = 'public_urls';
static final columnUpdationTime = 'updation_time';
static final columnIsDeleted = 'is_deleted';
@ -35,6 +36,7 @@ class CollectionsDB {
...addEncryptedName(),
...addVersion(),
...addIsDeleted(),
...addPublicURLs(),
];
final dbConfig = MigrationConfig(
@ -127,6 +129,15 @@ class CollectionsDB {
];
}
static List<String> addPublicURLs() {
return [
'''
ALTER TABLE $table
ADD COLUMN $columnPublicURLs TEXT;
'''
];
}
Future<List<dynamic>> insert(List<Collection> collections) async {
final db = await instance.database;
var batch = db.batch();
@ -185,6 +196,8 @@ class CollectionsDB {
row[columnVersion] = collection.attributes.version;
row[columnSharees] =
json.encode(collection.sharees?.map((x) => x?.toMap())?.toList());
row[columnPublicURLs] =
json.encode(collection.publicURLs?.map((x) => x?.toMap())?.toList());
row[columnUpdationTime] = collection.updationTime;
if (collection.isDeleted ?? false) {
row[columnIsDeleted] = _sqlBoolTrue;
@ -211,6 +224,8 @@ class CollectionsDB {
),
List<User>.from((json.decode(row[columnSharees]) as List)
.map((x) => User.fromMap(x))),
List<PublicURL>.from((json.decode(row[columnPublicURLs]) as List)
.map((x) => PublicURL.fromMap(x))),
int.parse(row[columnUpdationTime]),
// default to False is columnIsDeleted is not set
isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,

View file

@ -13,6 +13,7 @@ class Collection {
final CollectionType type;
final CollectionAttributes attributes;
final List<User> sharees;
final List<PublicURL> publicURLs;
final int updationTime;
final bool isDeleted;
@ -27,6 +28,7 @@ class Collection {
this.type,
this.attributes,
this.sharees,
this.publicURLs,
this.updationTime, {
this.isDeleted = false,
});
@ -63,6 +65,7 @@ class Collection {
CollectionType type,
CollectionAttributes attributes,
List<User> sharees,
List<PublicURL> publicURLs,
int updationTime,
bool isDeleted,
}) {
@ -77,6 +80,7 @@ class Collection {
type ?? this.type,
attributes ?? this.attributes,
sharees ?? this.sharees,
publicURLs ?? this.publicURLs,
updationTime ?? this.updationTime,
isDeleted: isDeleted ?? this.isDeleted,
);
@ -94,6 +98,7 @@ class Collection {
'type': typeToString(type),
'attributes': attributes?.toMap(),
'sharees': sharees?.map((x) => x?.toMap())?.toList(),
'publicURLs': publicURLs?.map((x) => x?.toMap())?.toList(),
'updationTime': updationTime,
'isDeleted': isDeleted,
};
@ -104,6 +109,11 @@ class Collection {
final sharees = (map['sharees'] == null || map['sharees'].length == 0)
? <User>[]
: List<User>.from(map['sharees'].map((x) => User.fromMap(x)));
final publicURLs =
(map['publicURLs'] == null || map['publicURLs'].length == 0)
? <PublicURL>[]
: List<PublicURL>.from(
map['publicURLs'].map((x) => PublicURL.fromMap(x)));
return Collection(
map['id'],
User.fromMap(map['owner']),
@ -115,6 +125,7 @@ class Collection {
typeFromString(map['type']),
CollectionAttributes.fromMap(map['attributes']),
sharees,
publicURLs,
map['updationTime'],
isDeleted: map['isDeleted'] ?? false,
);
@ -127,7 +138,7 @@ class Collection {
@override
String toString() {
return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, encryptedName: $encryptedName, nameDecryptionNonce: $nameDecryptionNonce, type: $type, attributes: $attributes, sharees: $sharees, updationTime: $updationTime, isDeleted: $isDeleted)';
return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, encryptedName: $encryptedName, nameDecryptionNonce: $nameDecryptionNonce, type: $type, attributes: $attributes, sharees: $sharees, publicURLs: $publicURLs, updationTime: $updationTime, isDeleted: $isDeleted)';
}
@override
@ -145,6 +156,7 @@ class Collection {
o.type == type &&
o.attributes == attributes &&
listEquals(o.sharees, sharees) &&
listEquals(o.publicURLs, publicURLs) &&
o.updationTime == updationTime &&
o.isDeleted == isDeleted;
}
@ -161,6 +173,7 @@ class Collection {
type.hashCode ^
attributes.hashCode ^
sharees.hashCode ^
publicURLs.hashCode ^
updationTime.hashCode ^
isDeleted.hashCode;
}
@ -299,3 +312,55 @@ class User {
@override
int get hashCode => id.hashCode ^ email.hashCode ^ name.hashCode;
}
class PublicURL {
String url;
int deviceLimit;
int validTill;
PublicURL({
this.url,
this.deviceLimit,
this.validTill,
});
Map<String, dynamic> toMap() {
return {
'url': url,
'deviceLimit': deviceLimit,
'validTill': validTill,
};
}
factory PublicURL.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return PublicURL(
url: map['url'],
deviceLimit: map['deviceLimit'],
validTill: map['validTill'],
);
}
String toJson() => json.encode(toMap());
factory PublicURL.fromJson(String source) =>
PublicURL.fromMap(json.decode(source));
@override
String toString() =>
'PublicUrl( url: $url, deviceLimit: $deviceLimit, validTill: $validTill)';
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is PublicURL &&
o.deviceLimit == deviceLimit &&
o.url == url &&
o.validTill == validTill;
}
@override
int get hashCode => deviceLimit.hashCode ^ url.hashCode ^ validTill.hashCode;
}

View file

@ -273,6 +273,46 @@ class CollectionsService {
}
}
Future<void> createShareUrl(Collection collection) async {
try {
final response = await _dio.post(
Configuration.instance.getHttpEndpoint() + "/collections/share-url",
data: {
"collectionID": collection.id,
},
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
);
collection.publicURLs?.add(PublicURL.fromMap(response.data["result"]));
await _db.insert(List.from([collection]));
_cacheCollectionAttributes(collection);
} catch (e, s) {
_logger.severe("failed to rename collection", e, s);
rethrow;
}
}
Future<void> disableShareUrl(Collection collection) async {
try {
await _dio.delete(
_config.getHttpEndpoint() +
"/collections/share-url/" +
collection.id.toString(),
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
},
),
);
collection.publicURLs.clear();
await _db.insert(List.from([collection]));
_cacheCollectionAttributes(collection);
} on DioError catch (e) {
_logger.info(e);
rethrow;
}
}
Future<List<Collection>> _fetchCollections(int sinceTime) async {
try {
final response = await _dio.get(
@ -320,6 +360,7 @@ class CollectionsService {
CollectionAttributes(),
null,
null,
null,
));
return collection;
}
@ -370,6 +411,7 @@ class CollectionsService {
),
null,
null,
null,
));
return collection;
}

View file

@ -96,6 +96,7 @@ class FavoritesService {
CollectionAttributes(),
null,
null,
null,
));
_cachedFavoritesCollectionID = collection.id;
return collection.id;

View file

@ -1,5 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
@ -80,6 +85,46 @@ class _SharingDialogState extends State<SharingDialog> {
);
}
bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false;
children.addAll([
Padding(padding: EdgeInsets.all(2)),
Divider(height: 12),
Padding(padding: EdgeInsets.all(Platform.isIOS ? 2 : 4)),
SizedBox(
height: 36,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("enable public url"),
Switch(
value: hasUrl,
onChanged: (enable) async {
final dialog = createProgressDialog(
context, enable ? "generating url..." : "disabling url");
try {
await dialog.show();
enable
? await CollectionsService.instance
.createShareUrl(widget.collection)
: await CollectionsService.instance
.disableShareUrl(widget.collection);
} catch (e) {
_logger.severe('failed to $enable url', e);
} finally {
dialog.hide();
setState(() {});
}
},
),
],
),
),
]);
if (widget.collection.publicURLs?.isNotEmpty ?? false) {
children.add(_getShareableUrlWidget());
}
return AlertDialog(
title: Text("sharing"),
content: SingleChildScrollView(
@ -148,6 +193,41 @@ class _SharingDialogState extends State<SharingDialog> {
);
}
Widget _getShareableUrlWidget() {
String base64urlEncode = base64UrlEncode(
CollectionsService.instance.getCollectionKey(widget.collection.id));
String url =
"${widget.collection.publicURLs.first.url}#collectionKey=$base64urlEncode";
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () async {
await Clipboard.setData(ClipboardData(text: url));
showToast("URL is copied to clipboard");
},
child: Container(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
url,
style: TextStyle(
fontSize: 16,
fontFeatures: const [FontFeature.tabularFigures()],
color: Colors.white.withOpacity(0.7),
),
),
),
color: Colors.white.withOpacity(0.1),
),
),
],
),
);
}
Future<void> _addEmailToCollection(
String email, {
String publicKey,