Skip to content

Commit

Permalink
feat: keep timestamps of transferred files (#1505)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto authored Jul 14, 2024
1 parent a95e617 commit 9d523b4
Show file tree
Hide file tree
Showing 27 changed files with 550 additions and 719 deletions.
1 change: 1 addition & 0 deletions app/assets/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- feat: add clear button in the send tab (@Caesarovich)
- feat: save text messages to history (@Tienisto)
- feat: keep timestamps of transferred files (@Tienisto)
- feat: add option to require PIN when sharing via link (@Tienisto)
- feat: add option to require PIN when receiving files (@Tienisto)
- feat: add option to open parent folder of received files in history (@Tienisto)
Expand Down
4 changes: 4 additions & 0 deletions app/lib/model/cross_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class CrossFile with CrossFileMappable {
final AssetEntity? asset; // for thumbnails
final String? path;
final List<int>? bytes; // if type message, then UTF-8 encoded
final DateTime? lastModified;
final DateTime? lastAccessed;

const CrossFile({
required this.name,
Expand All @@ -26,6 +28,8 @@ class CrossFile with CrossFileMappable {
required this.asset,
required this.path,
required this.bytes,
required this.lastModified,
required this.lastAccessed,
});

/// Custom toString() to avoid printing the bytes.
Expand Down
33 changes: 28 additions & 5 deletions app/lib/model/cross_file.mapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class CrossFileMapper extends ClassMapperBase<CrossFile> {
static const Field<CrossFile, String> _f$path = Field('path', _$path);
static List<int>? _$bytes(CrossFile v) => v.bytes;
static const Field<CrossFile, List<int>> _f$bytes = Field('bytes', _$bytes);
static DateTime? _$lastModified(CrossFile v) => v.lastModified;
static const Field<CrossFile, DateTime> _f$lastModified = Field('lastModified', _$lastModified);
static DateTime? _$lastAccessed(CrossFile v) => v.lastAccessed;
static const Field<CrossFile, DateTime> _f$lastAccessed = Field('lastAccessed', _$lastAccessed);

@override
final MappableFields<CrossFile> fields = const {
Expand All @@ -45,6 +49,8 @@ class CrossFileMapper extends ClassMapperBase<CrossFile> {
#asset: _f$asset,
#path: _f$path,
#bytes: _f$bytes,
#lastModified: _f$lastModified,
#lastAccessed: _f$lastAccessed,
};

static CrossFile _instantiate(DecodingData data) {
Expand All @@ -55,7 +61,9 @@ class CrossFileMapper extends ClassMapperBase<CrossFile> {
thumbnail: data.dec(_f$thumbnail),
asset: data.dec(_f$asset),
path: data.dec(_f$path),
bytes: data.dec(_f$bytes));
bytes: data.dec(_f$bytes),
lastModified: data.dec(_f$lastModified),
lastAccessed: data.dec(_f$lastAccessed));
}

@override
Expand Down Expand Up @@ -102,7 +110,16 @@ extension CrossFileValueCopy<$R, $Out> on ObjectCopyWith<$R, CrossFile, $Out> {

abstract class CrossFileCopyWith<$R, $In extends CrossFile, $Out> implements ClassCopyWith<$R, $In, $Out> {
ListCopyWith<$R, int, ObjectCopyWith<$R, int, int>>? get bytes;
$R call({String? name, FileType? fileType, int? size, Uint8List? thumbnail, AssetEntity? asset, String? path, List<int>? bytes});
$R call(
{String? name,
FileType? fileType,
int? size,
Uint8List? thumbnail,
AssetEntity? asset,
String? path,
List<int>? bytes,
DateTime? lastModified,
DateTime? lastAccessed});
CrossFileCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}

Expand All @@ -122,15 +139,19 @@ class _CrossFileCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, CrossFile,
Object? thumbnail = $none,
Object? asset = $none,
Object? path = $none,
Object? bytes = $none}) =>
Object? bytes = $none,
Object? lastModified = $none,
Object? lastAccessed = $none}) =>
$apply(FieldCopyWithData({
if (name != null) #name: name,
if (fileType != null) #fileType: fileType,
if (size != null) #size: size,
if (thumbnail != $none) #thumbnail: thumbnail,
if (asset != $none) #asset: asset,
if (path != $none) #path: path,
if (bytes != $none) #bytes: bytes
if (bytes != $none) #bytes: bytes,
if (lastModified != $none) #lastModified: lastModified,
if (lastAccessed != $none) #lastAccessed: lastAccessed
}));
@override
CrossFile $make(CopyWithData data) => CrossFile(
Expand All @@ -140,7 +161,9 @@ class _CrossFileCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, CrossFile,
thumbnail: data.get(#thumbnail, or: $value.thumbnail),
asset: data.get(#asset, or: $value.asset),
path: data.get(#path, or: $value.path),
bytes: data.get(#bytes, or: $value.bytes));
bytes: data.get(#bytes, or: $value.bytes),
lastModified: data.get(#lastModified, or: $value.lastModified),
lastAccessed: data.get(#lastAccessed, or: $value.lastAccessed));

@override
CrossFileCopyWith<$R2, CrossFile, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t) => _CrossFileCopyWithImpl($value, $cast, t);
Expand Down
6 changes: 6 additions & 0 deletions app/lib/provider/network/send_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
preview: files.length == 1 && files.first.fileType == FileType.text && files.first.bytes != null
? utf8.decode(files.first.bytes!) // send simple message by embedding it into the preview
: null,
metadata: file.lastModified != null || file.lastAccessed != null
? FileMetadata(
lastModified: file.lastModified,
lastAccessed: file.lastAccessed,
)
: null,
legacy: target.version == '1.0',
),
status: FileStatus.queue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ class ReceiveController {
isImage: fileType == FileType.image,
stream: request.read(),
androidSdkInt: server.ref.read(deviceInfoProvider).androidSdkInt,
lastModified: receivingFile.file.metadata?.lastModified,
lastAccessed: receivingFile.file.metadata?.lastAccessed,
onProgress: (savedBytes) {
if (receivingFile.file.size != 0) {
server.ref.notifier(progressProvider).setProgress(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ class SendController {
preview: files.first.fileType == FileType.text && files.first.bytes != null
? utf8.decode(files.first.bytes!) // send simple message by embedding it into the preview
: null,
metadata: file.lastModified != null || file.lastAccessed != null
? FileMetadata(
lastModified: file.lastModified,
lastAccessed: file.lastAccessed,
)
: null,
legacy: false,
),
asset: file.asset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class AddMessageAction extends ReduxAction<SelectedSendingFilesNotifier, List<Cr
asset: null,
path: null,
bytes: bytes,
lastModified: null,
lastAccessed: null,
);

return List.unmodifiable([
Expand Down Expand Up @@ -98,6 +100,8 @@ class AddBinaryAction extends ReduxAction<SelectedSendingFilesNotifier, List<Cro
asset: null,
path: null,
bytes: bytes,
lastModified: null,
lastAccessed: null,
);

return List.unmodifiable([
Expand Down Expand Up @@ -160,6 +164,8 @@ class AddDirectoryAction extends AsyncReduxAction<SelectedSendingFilesNotifier,
asset: null,
path: entity.path,
bytes: null,
lastModified: entity.lastModifiedSync().toUtc(),
lastAccessed: entity.lastAccessedSync().toUtc(),
);

final isAlreadySelect = state.any((element) => element.isSameFile(otherFile: file));
Expand Down Expand Up @@ -219,6 +225,8 @@ class AddAndroidDirectoryAction extends AsyncReduxAction<SelectedSendingFilesNot
asset: null,
path: file.uri,
bytes: null,
lastModified: DateTime.fromMillisecondsSinceEpoch(file.lastModified, isUtc: true),
lastAccessed: null,
);

final isAlreadySelect = state.any((element) => element.isSameFile(otherFile: crossFile));
Expand Down
25 changes: 12 additions & 13 deletions app/lib/util/native/cross_file_converters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:io';

import 'package:common/common.dart';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart' as file_picker;
import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
import 'package:localsend_app/model/cross_file.dart';
Expand All @@ -13,18 +12,6 @@ import 'package:wechat_assets_picker/wechat_assets_picker.dart';

/// Utility functions to convert third party models to common [CrossFile] model.
class CrossFileConverters {
static Future<CrossFile> convertPlatformFile(file_picker.PlatformFile file) async {
return CrossFile(
name: file.name,
fileType: file.name.guessFileType(),
size: file.size,
thumbnail: null,
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? file.bytes! : null,
);
}

static Future<CrossFile> convertAssetEntity(AssetEntity asset) async {
final file = (await asset.originFile)!;
return CrossFile(
Expand All @@ -35,6 +22,8 @@ class CrossFileConverters {
asset: asset,
path: file.path,
bytes: null,
lastModified: file.lastModifiedSync().toUtc(),
lastAccessed: file.lastAccessedSync().toUtc(),
);
}

Expand All @@ -47,6 +36,8 @@ class CrossFileConverters {
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? await file.readAsBytes() : null, // we can fetch it now because in Web it is already there
lastModified: kIsWeb ? null : await file.lastModified(),
lastAccessed: null,
);
}

Expand All @@ -59,6 +50,8 @@ class CrossFileConverters {
asset: null,
path: file.path,
bytes: null,
lastModified: file.lastModifiedSync().toUtc(),
lastAccessed: file.lastAccessedSync().toUtc(),
);
}

Expand All @@ -71,6 +64,8 @@ class CrossFileConverters {
asset: null,
path: file.uri,
bytes: null,
lastModified: DateTime.fromMillisecondsSinceEpoch(file.lastModified, isUtc: true),
lastAccessed: null,
);
}

Expand All @@ -85,6 +80,8 @@ class CrossFileConverters {
asset: null,
path: file.path,
bytes: null,
lastModified: file.lastModifiedSync().toUtc(),
lastAccessed: file.lastAccessedSync().toUtc(),
);
}

Expand All @@ -98,6 +95,8 @@ class CrossFileConverters {
asset: null,
path: app.apkFilePath,
bytes: null,
lastModified: null,
lastAccessed: null,
);
}
}
Expand Down
19 changes: 17 additions & 2 deletions app/lib/util/native/file_saver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Future<void> saveFile({
required bool isImage,
required Stream<List<int>> stream,
required int? androidSdkInt,
required DateTime? lastModified,
required DateTime? lastAccessed,
required void Function(int savedBytes) onProgress,
}) async {
if (!saveToGallery && androidSdkInt != null && androidSdkInt <= 29) {
Expand Down Expand Up @@ -51,7 +53,8 @@ Future<void> saveFile({
}
}

final sink = File(destinationPath).openWrite();
final file = File(destinationPath);
final sink = file.openWrite();
await _saveFile(
destinationPath: destinationPath,
saveToGallery: saveToGallery,
Expand All @@ -60,7 +63,19 @@ Future<void> saveFile({
onProgress: onProgress,
write: sink.add,
writeAsync: null,
close: sink.close,
close: () async {
await sink.close();
if (lastModified != null) {
try {
await file.setLastModified(lastModified);
} catch (_) {}
}
if (lastAccessed != null) {
try {
await file.setLastAccessed(lastAccessed);
} catch (_) {}
}
},
);
}

Expand Down
15 changes: 13 additions & 2 deletions app/test/unit/model/dto/prepare_upload_request_dto_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ void main() {
hash: '*hash*',
preview: '*preview data*',
legacy: true,
metadata: null,
),
'some id 2': FileDto(
id: 'some id 2',
Expand All @@ -165,6 +166,7 @@ void main() {
hash: '*hash*',
preview: '*preview data*',
legacy: true,
metadata: null,
),
},
);
Expand All @@ -176,17 +178,18 @@ void main() {
});

test('should serialize in mime mode', () {
const dto = PrepareUploadRequestDto(
final dto = PrepareUploadRequestDto(
info: info,
files: {
'some id': FileDto(
'some id': const FileDto(
id: 'some id',
fileName: 'another image.jpg',
size: 1234,
fileType: FileType.image,
hash: '*hash*',
preview: '*preview data*',
legacy: false,
metadata: null,
),
'some id 2': FileDto(
id: 'some id 2',
Expand All @@ -196,6 +199,10 @@ void main() {
hash: '*hash*',
preview: '*preview data*',
legacy: false,
metadata: FileMetadata(
lastModified: DateTime.utc(2020),
lastAccessed: DateTime.utc(2021),
),
),
},
);
Expand All @@ -205,6 +212,10 @@ void main() {
expect(serialized['files'].length, 2);
expect(serialized['files']['some id']['fileType'], 'image/jpeg');
expect(serialized['files']['some id 2']['fileType'], 'application/vnd.android.package-archive');
expect(serialized['files']['some id 2']['metadata'], {
'modified': '2020-01-01T00:00:00.000Z',
'accessed': '2021-01-01T00:00:00.000Z',
});
});
});

Expand Down
Loading

0 comments on commit 9d523b4

Please sign in to comment.