mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: add option to disable checksum verification
This commit is contained in:
@@ -120,11 +120,13 @@
|
|||||||
"destination": "Save to folder",
|
"destination": "Save to folder",
|
||||||
"downloads": "(Downloads)",
|
"downloads": "(Downloads)",
|
||||||
"saveToGallery": "Save media to gallery",
|
"saveToGallery": "Save media to gallery",
|
||||||
"saveToHistory": "Save to history"
|
"saveToHistory": "Save to history",
|
||||||
|
"verifyChecksums": "Verify checksums when receiving files"
|
||||||
},
|
},
|
||||||
"send": {
|
"send": {
|
||||||
"title": "Send",
|
"title": "Send",
|
||||||
"shareViaLinkAutoAccept": "Automatically accept requests in \"Share via link\" mode"
|
"shareViaLinkAutoAccept": "Automatically accept requests in \"Share via link\" mode",
|
||||||
|
"createChecksums": "Create checksums when sending files"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 55
|
/// Locales: 55
|
||||||
/// Strings: 18451 (335 per locale)
|
/// Strings: 18453 (335 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -1088,6 +1088,9 @@ class Translations$settingsTab$receive$en {
|
|||||||
|
|
||||||
/// en: 'Save to history'
|
/// en: 'Save to history'
|
||||||
String get saveToHistory => 'Save to history';
|
String get saveToHistory => 'Save to history';
|
||||||
|
|
||||||
|
/// en: 'Verify checksums when receiving files'
|
||||||
|
String get verifyChecksums => 'Verify checksums when receiving files';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: settingsTab.send
|
// Path: settingsTab.send
|
||||||
@@ -1103,6 +1106,9 @@ class Translations$settingsTab$send$en {
|
|||||||
|
|
||||||
/// en: 'Automatically accept requests in "Share via link" mode'
|
/// en: 'Automatically accept requests in "Share via link" mode'
|
||||||
String get shareViaLinkAutoAccept => 'Automatically accept requests in "Share via link" mode';
|
String get shareViaLinkAutoAccept => 'Automatically accept requests in "Share via link" mode';
|
||||||
|
|
||||||
|
/// en: 'Create checksums when sending files'
|
||||||
|
String get createChecksums => 'Create checksums when sending files';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: settingsTab.network
|
// Path: settingsTab.network
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class SettingsState with SettingsStateMappable {
|
|||||||
final String? deviceModel;
|
final String? deviceModel;
|
||||||
final bool shareViaLinkAutoAccept;
|
final bool shareViaLinkAutoAccept;
|
||||||
final bool receiveViaLinkAutoAccept;
|
final bool receiveViaLinkAutoAccept;
|
||||||
|
final bool createChecksums; // create checksums when sending files
|
||||||
|
final bool verifyChecksums; // verify checksums when receiving files
|
||||||
final int discoveryTimeout;
|
final int discoveryTimeout;
|
||||||
final bool advancedSettings;
|
final bool advancedSettings;
|
||||||
|
|
||||||
@@ -63,6 +65,8 @@ class SettingsState with SettingsStateMappable {
|
|||||||
required this.deviceModel,
|
required this.deviceModel,
|
||||||
required this.shareViaLinkAutoAccept,
|
required this.shareViaLinkAutoAccept,
|
||||||
required this.receiveViaLinkAutoAccept,
|
required this.receiveViaLinkAutoAccept,
|
||||||
|
required this.createChecksums,
|
||||||
|
required this.verifyChecksums,
|
||||||
required this.discoveryTimeout,
|
required this.discoveryTimeout,
|
||||||
required this.advancedSettings,
|
required this.advancedSettings,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -144,6 +144,16 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
|
|||||||
'receiveViaLinkAutoAccept',
|
'receiveViaLinkAutoAccept',
|
||||||
_$receiveViaLinkAutoAccept,
|
_$receiveViaLinkAutoAccept,
|
||||||
);
|
);
|
||||||
|
static bool _$createChecksums(SettingsState v) => v.createChecksums;
|
||||||
|
static const Field<SettingsState, bool> _f$createChecksums = Field(
|
||||||
|
'createChecksums',
|
||||||
|
_$createChecksums,
|
||||||
|
);
|
||||||
|
static bool _$verifyChecksums(SettingsState v) => v.verifyChecksums;
|
||||||
|
static const Field<SettingsState, bool> _f$verifyChecksums = Field(
|
||||||
|
'verifyChecksums',
|
||||||
|
_$verifyChecksums,
|
||||||
|
);
|
||||||
static int _$discoveryTimeout(SettingsState v) => v.discoveryTimeout;
|
static int _$discoveryTimeout(SettingsState v) => v.discoveryTimeout;
|
||||||
static const Field<SettingsState, int> _f$discoveryTimeout = Field(
|
static const Field<SettingsState, int> _f$discoveryTimeout = Field(
|
||||||
'discoveryTimeout',
|
'discoveryTimeout',
|
||||||
@@ -182,6 +192,8 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
|
|||||||
#deviceModel: _f$deviceModel,
|
#deviceModel: _f$deviceModel,
|
||||||
#shareViaLinkAutoAccept: _f$shareViaLinkAutoAccept,
|
#shareViaLinkAutoAccept: _f$shareViaLinkAutoAccept,
|
||||||
#receiveViaLinkAutoAccept: _f$receiveViaLinkAutoAccept,
|
#receiveViaLinkAutoAccept: _f$receiveViaLinkAutoAccept,
|
||||||
|
#createChecksums: _f$createChecksums,
|
||||||
|
#verifyChecksums: _f$verifyChecksums,
|
||||||
#discoveryTimeout: _f$discoveryTimeout,
|
#discoveryTimeout: _f$discoveryTimeout,
|
||||||
#advancedSettings: _f$advancedSettings,
|
#advancedSettings: _f$advancedSettings,
|
||||||
};
|
};
|
||||||
@@ -213,6 +225,8 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
|
|||||||
deviceModel: data.dec(_f$deviceModel),
|
deviceModel: data.dec(_f$deviceModel),
|
||||||
shareViaLinkAutoAccept: data.dec(_f$shareViaLinkAutoAccept),
|
shareViaLinkAutoAccept: data.dec(_f$shareViaLinkAutoAccept),
|
||||||
receiveViaLinkAutoAccept: data.dec(_f$receiveViaLinkAutoAccept),
|
receiveViaLinkAutoAccept: data.dec(_f$receiveViaLinkAutoAccept),
|
||||||
|
createChecksums: data.dec(_f$createChecksums),
|
||||||
|
verifyChecksums: data.dec(_f$verifyChecksums),
|
||||||
discoveryTimeout: data.dec(_f$discoveryTimeout),
|
discoveryTimeout: data.dec(_f$discoveryTimeout),
|
||||||
advancedSettings: data.dec(_f$advancedSettings),
|
advancedSettings: data.dec(_f$advancedSettings),
|
||||||
);
|
);
|
||||||
@@ -310,6 +324,8 @@ abstract class SettingsStateCopyWith<$R, $In extends SettingsState, $Out>
|
|||||||
String? deviceModel,
|
String? deviceModel,
|
||||||
bool? shareViaLinkAutoAccept,
|
bool? shareViaLinkAutoAccept,
|
||||||
bool? receiveViaLinkAutoAccept,
|
bool? receiveViaLinkAutoAccept,
|
||||||
|
bool? createChecksums,
|
||||||
|
bool? verifyChecksums,
|
||||||
int? discoveryTimeout,
|
int? discoveryTimeout,
|
||||||
bool? advancedSettings,
|
bool? advancedSettings,
|
||||||
});
|
});
|
||||||
@@ -369,6 +385,8 @@ class _SettingsStateCopyWithImpl<$R, $Out>
|
|||||||
Object? deviceModel = $none,
|
Object? deviceModel = $none,
|
||||||
bool? shareViaLinkAutoAccept,
|
bool? shareViaLinkAutoAccept,
|
||||||
bool? receiveViaLinkAutoAccept,
|
bool? receiveViaLinkAutoAccept,
|
||||||
|
bool? createChecksums,
|
||||||
|
bool? verifyChecksums,
|
||||||
int? discoveryTimeout,
|
int? discoveryTimeout,
|
||||||
bool? advancedSettings,
|
bool? advancedSettings,
|
||||||
}) => $apply(
|
}) => $apply(
|
||||||
@@ -402,6 +420,8 @@ class _SettingsStateCopyWithImpl<$R, $Out>
|
|||||||
#shareViaLinkAutoAccept: shareViaLinkAutoAccept,
|
#shareViaLinkAutoAccept: shareViaLinkAutoAccept,
|
||||||
if (receiveViaLinkAutoAccept != null)
|
if (receiveViaLinkAutoAccept != null)
|
||||||
#receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
|
#receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
|
||||||
|
if (createChecksums != null) #createChecksums: createChecksums,
|
||||||
|
if (verifyChecksums != null) #verifyChecksums: verifyChecksums,
|
||||||
if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout,
|
if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout,
|
||||||
if (advancedSettings != null) #advancedSettings: advancedSettings,
|
if (advancedSettings != null) #advancedSettings: advancedSettings,
|
||||||
}),
|
}),
|
||||||
@@ -445,6 +465,8 @@ class _SettingsStateCopyWithImpl<$R, $Out>
|
|||||||
#receiveViaLinkAutoAccept,
|
#receiveViaLinkAutoAccept,
|
||||||
or: $value.receiveViaLinkAutoAccept,
|
or: $value.receiveViaLinkAutoAccept,
|
||||||
),
|
),
|
||||||
|
createChecksums: data.get(#createChecksums, or: $value.createChecksums),
|
||||||
|
verifyChecksums: data.get(#verifyChecksums, or: $value.verifyChecksums),
|
||||||
discoveryTimeout: data.get(#discoveryTimeout, or: $value.discoveryTimeout),
|
discoveryTimeout: data.get(#discoveryTimeout, or: $value.discoveryTimeout),
|
||||||
advancedSettings: data.get(#advancedSettings, or: $value.advancedSettings),
|
advancedSettings: data.get(#advancedSettings, or: $value.advancedSettings),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -259,6 +259,19 @@ class SettingsTab extends StatelessWidget {
|
|||||||
await ref.notifier(settingsProvider).setSaveToHistory(b);
|
await ref.notifier(settingsProvider).setSaveToHistory(b);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (vm.advanced)
|
||||||
|
_BooleanEntry(
|
||||||
|
label: t.settingsTab.receive.verifyChecksums,
|
||||||
|
value: vm.settings.verifyChecksums,
|
||||||
|
onChanged: (b) async {
|
||||||
|
await ref.notifier(settingsProvider).setVerifyChecksums(b);
|
||||||
|
|
||||||
|
// The checksums are verified by the Rust server, so it needs a restart.
|
||||||
|
if (ref.read(serverProvider) != null) {
|
||||||
|
await ref.notifier(serverProvider).restartServerFromSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (vm.advanced)
|
if (vm.advanced)
|
||||||
@@ -272,6 +285,13 @@ class SettingsTab extends StatelessWidget {
|
|||||||
await ref.notifier(settingsProvider).setShareViaLinkAutoAccept(b);
|
await ref.notifier(settingsProvider).setShareViaLinkAutoAccept(b);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_BooleanEntry(
|
||||||
|
label: t.settingsTab.send.createChecksums,
|
||||||
|
value: vm.settings.createChecksums,
|
||||||
|
onChanged: (b) async {
|
||||||
|
await ref.notifier(settingsProvider).setCreateChecksums(b);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_SettingsSection(
|
_SettingsSection(
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
// if someone else answers on that address.
|
// if someone else answers on that address.
|
||||||
final client = ref.read(httpProvider).pinnedTo(target.fingerprint);
|
final client = ref.read(httpProvider).pinnedTo(target.fingerprint);
|
||||||
final sessionId = _uuid.v4();
|
final sessionId = _uuid.v4();
|
||||||
|
final createChecksums = ref.read(settingsProvider).createChecksums;
|
||||||
|
|
||||||
// The ids are assigned upfront, so the checksums calculated below
|
// The ids are assigned upfront, so the checksums calculated below
|
||||||
// can be mapped back to the corresponding file.
|
// can be mapped back to the corresponding file.
|
||||||
@@ -116,7 +117,9 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
hashedFileCount: 0,
|
// Skipping the checksums marks all files as hashed, so the UI does not
|
||||||
|
// show the checksum progress.
|
||||||
|
hashedFileCount: createChecksums ? 0 : selectedFiles.length,
|
||||||
startTime: null,
|
startTime: null,
|
||||||
endTime: null,
|
endTime: null,
|
||||||
sendingTasks: [],
|
sendingTasks: [],
|
||||||
@@ -134,54 +137,56 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
|||||||
|
|
||||||
// Calculate the checksums which are part of the request.
|
// Calculate the checksums which are part of the request.
|
||||||
// The files are read and hashed in Rust, one file after another.
|
// The files are read and hashed in Rust, one file after another.
|
||||||
final hashCancelToken = rust_cancel.createCancellationToken();
|
|
||||||
_hashCancelTokens[sessionId] = hashCancelToken;
|
|
||||||
final hashes = <String, String>{};
|
final hashes = <String, String>{};
|
||||||
try {
|
if (createChecksums) {
|
||||||
for (final (:id, :file) in selectedFiles) {
|
final hashCancelToken = rust_cancel.createCancellationToken();
|
||||||
try {
|
_hashCancelTokens[sessionId] = hashCancelToken;
|
||||||
hashes[id] = await calculateFileHash(
|
try {
|
||||||
path: file.path,
|
for (final (:id, :file) in selectedFiles) {
|
||||||
bytes: file.bytes,
|
try {
|
||||||
cancelToken: hashCancelToken,
|
hashes[id] = await calculateFileHash(
|
||||||
onProgress: (bytes) {
|
path: file.path,
|
||||||
if (state[sessionId] == null) {
|
bytes: file.bytes,
|
||||||
// session has been canceled while calculating the checksums
|
cancelToken: hashCancelToken,
|
||||||
return;
|
onProgress: (bytes) {
|
||||||
}
|
if (state[sessionId] == null) {
|
||||||
ref
|
// session has been canceled while calculating the checksums
|
||||||
.notifier(progressProvider)
|
return;
|
||||||
.setProgress(
|
}
|
||||||
sessionId: sessionId,
|
ref
|
||||||
fileId: id,
|
.notifier(progressProvider)
|
||||||
progress: file.size == 0 ? 1 : (bytes / file.size).clamp(0, 1),
|
.setProgress(
|
||||||
);
|
sessionId: sessionId,
|
||||||
},
|
fileId: id,
|
||||||
);
|
progress: file.size == 0 ? 1 : (bytes / file.size).clamp(0, 1),
|
||||||
} catch (e) {
|
);
|
||||||
if (state[sessionId] != null) {
|
},
|
||||||
// Sending the checksum is optional, so a file that cannot be read
|
);
|
||||||
// here still gets a chance to be sent.
|
} catch (e) {
|
||||||
// Errors caused by the cancellation are not logged.
|
if (state[sessionId] != null) {
|
||||||
_logger.warning('Could not calculate the checksum of ${file.name}', e);
|
// Sending the checksum is optional, so a file that cannot be read
|
||||||
|
// here still gets a chance to be sent.
|
||||||
|
// Errors caused by the cancellation are not logged.
|
||||||
|
_logger.warning('Could not calculate the checksum of ${file.name}', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (state[sessionId] == null) {
|
if (state[sessionId] == null) {
|
||||||
// session has been canceled while calculating the checksums
|
// session has been canceled while calculating the checksums
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also set for files whose hashing failed, so the progress bar stays
|
// Also set for files whose hashing failed, so the progress bar stays
|
||||||
// consistent with the files that are left.
|
// consistent with the files that are left.
|
||||||
ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1);
|
ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1);
|
||||||
state = state.updateSession(
|
state = state.updateSession(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1),
|
state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_hashCancelTokens.remove(sessionId);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
_hashCancelTokens.remove(sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final hashedState = state[sessionId];
|
final hashedState = state[sessionId];
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class ServerService extends Notifier<ServerState?> {
|
|||||||
.dispatchTakeResult(
|
.dispatchTakeResult(
|
||||||
IsolateHttpServerStartAction(
|
IsolateHttpServerStartAction(
|
||||||
pin: settings.receivePin,
|
pin: settings.receivePin,
|
||||||
|
verifyChecksums: settings.verifyChecksums,
|
||||||
web: webSendState != null || webUpload
|
web: webSendState != null || webUpload
|
||||||
? WebParams(
|
? WebParams(
|
||||||
send: webSendState != null
|
send: webSendState != null
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ const _deviceType = 'ls_device_type';
|
|||||||
const _deviceModel = 'ls_device_model';
|
const _deviceModel = 'ls_device_model';
|
||||||
const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept';
|
const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept';
|
||||||
const _receiveViaLinkAutoAccept = 'ls_receive_via_link_auto_accept';
|
const _receiveViaLinkAutoAccept = 'ls_receive_via_link_auto_accept';
|
||||||
|
const _createChecksums = 'ls_create_checksums';
|
||||||
|
const _verifyChecksums = 'ls_verify_checksums';
|
||||||
const _advancedSettingsKey = 'ls_advanced_settings';
|
const _advancedSettingsKey = 'ls_advanced_settings';
|
||||||
const _whatsNewKey = 'ls_whats_new';
|
const _whatsNewKey = 'ls_whats_new';
|
||||||
|
|
||||||
@@ -377,6 +379,22 @@ class PersistenceService {
|
|||||||
await _prefs.setBool(_receiveViaLinkAutoAccept, receiveViaLinkAutoAccept);
|
await _prefs.setBool(_receiveViaLinkAutoAccept, receiveViaLinkAutoAccept);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool getCreateChecksums() {
|
||||||
|
return _prefs.getBool(_createChecksums) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setCreateChecksums(bool createChecksums) async {
|
||||||
|
await _prefs.setBool(_createChecksums, createChecksums);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getVerifyChecksums() {
|
||||||
|
return _prefs.getBool(_verifyChecksums) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setVerifyChecksums(bool verifyChecksums) async {
|
||||||
|
await _prefs.setBool(_verifyChecksums, verifyChecksums);
|
||||||
|
}
|
||||||
|
|
||||||
String getMulticastGroup() {
|
String getMulticastGroup() {
|
||||||
return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup;
|
return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ class SettingsService extends PureNotifier<SettingsState> {
|
|||||||
deviceModel: _persistence.getDeviceModel(),
|
deviceModel: _persistence.getDeviceModel(),
|
||||||
shareViaLinkAutoAccept: _persistence.getShareViaLinkAutoAccept(),
|
shareViaLinkAutoAccept: _persistence.getShareViaLinkAutoAccept(),
|
||||||
receiveViaLinkAutoAccept: _persistence.getReceiveViaLinkAutoAccept(),
|
receiveViaLinkAutoAccept: _persistence.getReceiveViaLinkAutoAccept(),
|
||||||
|
createChecksums: _persistence.getCreateChecksums(),
|
||||||
|
verifyChecksums: _persistence.getVerifyChecksums(),
|
||||||
discoveryTimeout: _persistence.getDiscoveryTimeout(),
|
discoveryTimeout: _persistence.getDiscoveryTimeout(),
|
||||||
advancedSettings: _persistence.getAdvancedSettingsEnabled(),
|
advancedSettings: _persistence.getAdvancedSettingsEnabled(),
|
||||||
);
|
);
|
||||||
@@ -273,4 +275,18 @@ class SettingsService extends PureNotifier<SettingsState> {
|
|||||||
receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
|
receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setCreateChecksums(bool createChecksums) async {
|
||||||
|
await _persistence.setCreateChecksums(createChecksums);
|
||||||
|
state = state.copyWith(
|
||||||
|
createChecksums: createChecksums,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setVerifyChecksums(bool verifyChecksums) async {
|
||||||
|
await _persistence.setVerifyChecksums(verifyChecksums);
|
||||||
|
state = state.copyWith(
|
||||||
|
verifyChecksums: verifyChecksums,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,42 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService
|
|||||||
)
|
)
|
||||||
as _i4.Future<void>);
|
as _i4.Future<void>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool getCreateChecksums() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#getCreateChecksums, []),
|
||||||
|
returnValue: false,
|
||||||
|
returnValueForMissingStub: false,
|
||||||
|
)
|
||||||
|
as bool);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i4.Future<void> setCreateChecksums(bool? createChecksums) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#setCreateChecksums, [createChecksums]),
|
||||||
|
returnValue: _i4.Future<void>.value(),
|
||||||
|
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||||
|
)
|
||||||
|
as _i4.Future<void>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool getVerifyChecksums() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#getVerifyChecksums, []),
|
||||||
|
returnValue: false,
|
||||||
|
returnValueForMissingStub: false,
|
||||||
|
)
|
||||||
|
as bool);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i4.Future<void> setVerifyChecksums(bool? verifyChecksums) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#setVerifyChecksums, [verifyChecksums]),
|
||||||
|
returnValue: _i4.Future<void>.value(),
|
||||||
|
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||||
|
)
|
||||||
|
as _i4.Future<void>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getMulticastGroup() =>
|
String getMulticastGroup() =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx: server_tx,
|
event_tx: server_tx,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ pub struct ServerConfigV2 {
|
|||||||
/// Optional PIN that senders must provide via the `pin` query parameter.
|
/// Optional PIN that senders must provide via the `pin` query parameter.
|
||||||
pub pin: Option<String>,
|
pub pin: Option<String>,
|
||||||
|
|
||||||
|
/// Whether the SHA-256 checksums that senders provide for their files are
|
||||||
|
/// verified after receiving. When disabled, received files are not hashed
|
||||||
|
/// and a mismatch is not detected.
|
||||||
|
pub verify_checksums: bool,
|
||||||
|
|
||||||
/// Channel on which the server emits events that must be handled by the application.
|
/// Channel on which the server emits events that must be handled by the application.
|
||||||
pub event_tx: mpsc::Sender<ServerEventV2>,
|
pub event_tx: mpsc::Sender<ServerEventV2>,
|
||||||
}
|
}
|
||||||
@@ -48,6 +53,9 @@ pub(crate) struct V2State {
|
|||||||
/// Optional PIN required for prepare-upload requests.
|
/// Optional PIN required for prepare-upload requests.
|
||||||
pub(crate) pin: Option<String>,
|
pub(crate) pin: Option<String>,
|
||||||
|
|
||||||
|
/// Whether sender-provided SHA-256 checksums are verified after receiving.
|
||||||
|
pub(crate) verify_checksums: bool,
|
||||||
|
|
||||||
/// Channel on which server events are emitted to the application.
|
/// Channel on which server events are emitted to the application.
|
||||||
pub(crate) event_tx: mpsc::Sender<ServerEventV2>,
|
pub(crate) event_tx: mpsc::Sender<ServerEventV2>,
|
||||||
|
|
||||||
@@ -95,6 +103,7 @@ impl AppState {
|
|||||||
let v2 = v2_config.map(|config| {
|
let v2 = v2_config.map(|config| {
|
||||||
Arc::new(V2State {
|
Arc::new(V2State {
|
||||||
pin: config.pin,
|
pin: config.pin,
|
||||||
|
verify_checksums: config.verify_checksums,
|
||||||
event_tx: config.event_tx,
|
event_tx: config.event_tx,
|
||||||
session: Mutex::new(None),
|
session: Mutex::new(None),
|
||||||
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
|
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
|
||||||
|
|||||||
@@ -385,7 +385,10 @@ pub(crate) async fn upload(
|
|||||||
let mut upload_guard = UploadGuard::new(v2.clone(), session_id.clone(), file_id.clone());
|
let mut upload_guard = UploadGuard::new(v2.clone(), session_id.clone(), file_id.clone());
|
||||||
|
|
||||||
let file_size = file_dto.size;
|
let file_size = file_dto.size;
|
||||||
let expected_sha256 = file_dto.sha256.clone();
|
let expected_sha256 = match v2.verify_checksums {
|
||||||
|
true => file_dto.sha256.clone(),
|
||||||
|
false => None,
|
||||||
|
};
|
||||||
let timestamps = match &file_dto.metadata {
|
let timestamps = match &file_dto.metadata {
|
||||||
Some(metadata) => FileTimestamps {
|
Some(metadata) => FileTimestamps {
|
||||||
modified: metadata.modified_time(),
|
modified: metadata.modified_time(),
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ async fn start_register_server(
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx,
|
event_tx,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ async fn start_test_server(
|
|||||||
pin: Option<String>,
|
pin: Option<String>,
|
||||||
accept: bool,
|
accept: bool,
|
||||||
save_dir: Option<PathBuf>,
|
save_dir: Option<PathBuf>,
|
||||||
|
) -> TestServer {
|
||||||
|
start_test_server_with_verification(pin, accept, save_dir, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [start_test_server], but allows disabling the checksum verification.
|
||||||
|
async fn start_test_server_with_verification(
|
||||||
|
pin: Option<String>,
|
||||||
|
accept: bool,
|
||||||
|
save_dir: Option<PathBuf>,
|
||||||
|
verify_checksums: bool,
|
||||||
) -> TestServer {
|
) -> TestServer {
|
||||||
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
|
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
|
||||||
let port = free_port();
|
let port = free_port();
|
||||||
@@ -125,7 +135,11 @@ async fn start_test_server(
|
|||||||
token: "server-fingerprint".to_string(),
|
token: "server-fingerprint".to_string(),
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 { pin, event_tx }),
|
Some(ServerConfigV2 {
|
||||||
|
pin,
|
||||||
|
verify_checksums,
|
||||||
|
event_tx,
|
||||||
|
}),
|
||||||
None,
|
None,
|
||||||
stop_rx,
|
stop_rx,
|
||||||
)
|
)
|
||||||
@@ -499,6 +513,45 @@ async fn test_upload_with_mismatched_sha256() {
|
|||||||
assert_status(result, 422);
|
assert_status(result, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_upload_mismatched_sha256_with_verification_disabled() {
|
||||||
|
let server = start_test_server_with_verification(None, true, None, false).await;
|
||||||
|
let client = LsHttpClientV2::try_new_without_cert().unwrap();
|
||||||
|
|
||||||
|
let bytes = b"hello".to_vec();
|
||||||
|
let mut file = file_dto("file-a", "a.bin", bytes.len() as u64);
|
||||||
|
file.sha256 = Some(sha256_hex(b"something else"));
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.prepare_upload(
|
||||||
|
ProtocolType::Http,
|
||||||
|
"127.0.0.1",
|
||||||
|
server.port,
|
||||||
|
None,
|
||||||
|
prepare_upload_request(&[file]),
|
||||||
|
None,
|
||||||
|
CancellationToken::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.response
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The mismatch goes unnoticed because the received bytes are not hashed.
|
||||||
|
upload_bytes(
|
||||||
|
&client,
|
||||||
|
server.port,
|
||||||
|
&response.session_id,
|
||||||
|
"file-a",
|
||||||
|
&response.files["file-a"],
|
||||||
|
&bytes,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(server.received.lock().await["file-a"], bytes);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_upload_retry_after_mismatched_sha256() {
|
async fn test_upload_retry_after_mismatched_sha256() {
|
||||||
let server = start_test_server(None, true, None).await;
|
let server = start_test_server(None, true, None).await;
|
||||||
@@ -927,6 +980,7 @@ async fn test_prepare_upload_aborted_by_sender_disconnect() {
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx,
|
event_tx,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
@@ -1029,6 +1083,7 @@ async fn test_prepare_upload_cancelled_by_session_less_cancel() {
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx,
|
event_tx,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
@@ -1164,6 +1219,7 @@ async fn test_prepare_upload_aborted_by_sender_disconnect_tls() {
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx,
|
event_tx,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ async fn start_tls_server_with_web(identity: &Identity, web: Option<WebConfig>)
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx,
|
event_tx,
|
||||||
}),
|
}),
|
||||||
web,
|
web,
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ async fn start_test_server(
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx: v2_event_tx,
|
event_tx: v2_event_tx,
|
||||||
}),
|
}),
|
||||||
web_config,
|
web_config,
|
||||||
@@ -340,6 +341,7 @@ async fn test_upload_page() {
|
|||||||
None,
|
None,
|
||||||
Some(ServerConfigV2 {
|
Some(ServerConfigV2 {
|
||||||
pin: None,
|
pin: None,
|
||||||
|
verify_checksums: true,
|
||||||
event_tx: v2_event_tx,
|
event_tx: v2_event_tx,
|
||||||
}),
|
}),
|
||||||
Some(WebConfig {
|
Some(WebConfig {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Future<RsHttpServer> startServer({
|
|||||||
DeviceType? deviceType,
|
DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
String? pin,
|
String? pin,
|
||||||
|
required bool verifyChecksums,
|
||||||
WebParams? web,
|
WebParams? web,
|
||||||
String? showToken,
|
String? showToken,
|
||||||
}) => RustLib.instance.api.crateApiServerStartServer(
|
}) => RustLib.instance.api.crateApiServerStartServer(
|
||||||
@@ -45,6 +46,7 @@ Future<RsHttpServer> startServer({
|
|||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
pin: pin,
|
pin: pin,
|
||||||
|
verifyChecksums: verifyChecksums,
|
||||||
web: web,
|
web: web,
|
||||||
showToken: showToken,
|
showToken: showToken,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
DeviceType? deviceType,
|
DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
String? pin,
|
String? pin,
|
||||||
|
required bool verifyChecksums,
|
||||||
WebParams? web,
|
WebParams? web,
|
||||||
String? showToken,
|
String? showToken,
|
||||||
});
|
});
|
||||||
@@ -2043,6 +2044,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
DeviceType? deviceType,
|
DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
String? pin,
|
String? pin,
|
||||||
|
required bool verifyChecksums,
|
||||||
WebParams? web,
|
WebParams? web,
|
||||||
String? showToken,
|
String? showToken,
|
||||||
}) {
|
}) {
|
||||||
@@ -2058,6 +2060,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_opt_box_autoadd_device_type(deviceType, serializer);
|
sse_encode_opt_box_autoadd_device_type(deviceType, serializer);
|
||||||
sse_encode_String(fingerprint, serializer);
|
sse_encode_String(fingerprint, serializer);
|
||||||
sse_encode_opt_String(pin, serializer);
|
sse_encode_opt_String(pin, serializer);
|
||||||
|
sse_encode_bool(verifyChecksums, serializer);
|
||||||
sse_encode_opt_box_autoadd_web_params(web, serializer);
|
sse_encode_opt_box_autoadd_web_params(web, serializer);
|
||||||
sse_encode_opt_String(showToken, serializer);
|
sse_encode_opt_String(showToken, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_);
|
||||||
@@ -2067,7 +2070,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
decodeErrorData: sse_decode_AnyhowException,
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
),
|
),
|
||||||
constMeta: kCrateApiServerStartServerConstMeta,
|
constMeta: kCrateApiServerStartServerConstMeta,
|
||||||
argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin, web, showToken],
|
argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin, verifyChecksums, web, showToken],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2075,7 +2078,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
|
|
||||||
TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta(
|
TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta(
|
||||||
debugName: 'start_server',
|
debugName: 'start_server',
|
||||||
argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin', 'web', 'showToken'],
|
argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin', 'verifyChecksums', 'web', 'showToken'],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class HttpServerStartTask implements BaseHttpServerTask {
|
|||||||
/// Optional PIN that senders must provide to start an upload session.
|
/// Optional PIN that senders must provide to start an upload session.
|
||||||
final String? pin;
|
final String? pin;
|
||||||
|
|
||||||
|
/// Whether the SHA-256 checksums that senders provide for their files are
|
||||||
|
/// verified after receiving.
|
||||||
|
final bool verifyChecksums;
|
||||||
|
|
||||||
/// Serves the web pages: the download page (web send) and/or the upload page.
|
/// Serves the web pages: the download page (web send) and/or the upload page.
|
||||||
/// `null` disables the web pages.
|
/// `null` disables the web pages.
|
||||||
final WebParams? web;
|
final WebParams? web;
|
||||||
@@ -40,6 +44,7 @@ class HttpServerStartTask implements BaseHttpServerTask {
|
|||||||
|
|
||||||
HttpServerStartTask({
|
HttpServerStartTask({
|
||||||
required this.pin,
|
required this.pin,
|
||||||
|
required this.verifyChecksums,
|
||||||
required this.web,
|
required this.web,
|
||||||
required this.showToken,
|
required this.showToken,
|
||||||
});
|
});
|
||||||
@@ -398,6 +403,7 @@ Future<void> setupHttpServerIsolate(
|
|||||||
deviceType: syncState.deviceInfo.deviceType.toRust(),
|
deviceType: syncState.deviceInfo.deviceType.toRust(),
|
||||||
fingerprint: syncState.securityContext.certificateHash,
|
fingerprint: syncState.securityContext.certificateHash,
|
||||||
pin: startTask.pin,
|
pin: startTask.pin,
|
||||||
|
verifyChecksums: startTask.verifyChecksums,
|
||||||
web: startTask.web,
|
web: startTask.web,
|
||||||
showToken: startTask.showToken,
|
showToken: startTask.showToken,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -256,6 +256,10 @@ class IsolateHttpUploadCancelAction extends ReduxAction<IsolateController, Paren
|
|||||||
class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> {
|
class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> {
|
||||||
final String? pin;
|
final String? pin;
|
||||||
|
|
||||||
|
/// Whether the SHA-256 checksums that senders provide for their files are
|
||||||
|
/// verified after receiving.
|
||||||
|
final bool verifyChecksums;
|
||||||
|
|
||||||
/// Serves the web pages: the download page (web send) and/or the upload page.
|
/// Serves the web pages: the download page (web send) and/or the upload page.
|
||||||
/// `null` disables the web pages.
|
/// `null` disables the web pages.
|
||||||
final WebParams? web;
|
final WebParams? web;
|
||||||
@@ -266,6 +270,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
|
|||||||
|
|
||||||
IsolateHttpServerStartAction({
|
IsolateHttpServerStartAction({
|
||||||
required this.pin,
|
required this.pin,
|
||||||
|
required this.verifyChecksums,
|
||||||
required this.web,
|
required this.web,
|
||||||
required this.showToken,
|
required this.showToken,
|
||||||
});
|
});
|
||||||
@@ -282,6 +287,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
|
|||||||
connection.sendWrappedTaskAndListenStream(
|
connection.sendWrappedTaskAndListenStream(
|
||||||
task: HttpServerStartTask(
|
task: HttpServerStartTask(
|
||||||
pin: pin,
|
pin: pin,
|
||||||
|
verifyChecksums: verifyChecksums,
|
||||||
web: web,
|
web: web,
|
||||||
showToken: showToken,
|
showToken: showToken,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class HttpServerService {
|
|||||||
required DeviceType? deviceType,
|
required DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
required String? pin,
|
required String? pin,
|
||||||
|
required bool verifyChecksums,
|
||||||
required WebParams? web,
|
required WebParams? web,
|
||||||
required String? showToken,
|
required String? showToken,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -38,6 +39,7 @@ class HttpServerService {
|
|||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
pin: pin,
|
pin: pin,
|
||||||
|
verifyChecksums: verifyChecksums,
|
||||||
web: web,
|
web: web,
|
||||||
showToken: showToken,
|
showToken: showToken,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ pub async fn start_server(
|
|||||||
device_type: Option<DeviceType>,
|
device_type: Option<DeviceType>,
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
pin: Option<String>,
|
pin: Option<String>,
|
||||||
|
verify_checksums: bool,
|
||||||
web: Option<WebParams>,
|
web: Option<WebParams>,
|
||||||
show_token: Option<String>,
|
show_token: Option<String>,
|
||||||
) -> anyhow::Result<RsHttpServer> {
|
) -> anyhow::Result<RsHttpServer> {
|
||||||
@@ -243,7 +244,11 @@ pub async fn start_server(
|
|||||||
token: fingerprint,
|
token: fingerprint,
|
||||||
},
|
},
|
||||||
internal_config,
|
internal_config,
|
||||||
Some(ServerConfigV2 { pin, event_tx }),
|
Some(ServerConfigV2 {
|
||||||
|
pin,
|
||||||
|
verify_checksums,
|
||||||
|
event_tx,
|
||||||
|
}),
|
||||||
web_config,
|
web_config,
|
||||||
stop_rx,
|
stop_rx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3201,6 +3201,7 @@ fn wire__crate__api__server__start_server_impl(
|
|||||||
<Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer);
|
<Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer);
|
||||||
let api_fingerprint = <String>::sse_decode(&mut deserializer);
|
let api_fingerprint = <String>::sse_decode(&mut deserializer);
|
||||||
let api_pin = <Option<String>>::sse_decode(&mut deserializer);
|
let api_pin = <Option<String>>::sse_decode(&mut deserializer);
|
||||||
|
let api_verify_checksums = <bool>::sse_decode(&mut deserializer);
|
||||||
let api_web = <Option<crate::api::server::WebParams>>::sse_decode(&mut deserializer);
|
let api_web = <Option<crate::api::server::WebParams>>::sse_decode(&mut deserializer);
|
||||||
let api_show_token = <Option<String>>::sse_decode(&mut deserializer);
|
let api_show_token = <Option<String>>::sse_decode(&mut deserializer);
|
||||||
deserializer.end();
|
deserializer.end();
|
||||||
@@ -3216,6 +3217,7 @@ fn wire__crate__api__server__start_server_impl(
|
|||||||
api_device_type,
|
api_device_type,
|
||||||
api_fingerprint,
|
api_fingerprint,
|
||||||
api_pin,
|
api_pin,
|
||||||
|
api_verify_checksums,
|
||||||
api_web,
|
api_web,
|
||||||
api_show_token,
|
api_show_token,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user