fix: create the destination directory when it is missing (#3223)

This commit is contained in:
Nurlan Garash
2026-08-01 16:25:16 +04:00
committed by GitHub
parent 7ec5d26294
commit bb919e3889
5 changed files with 150 additions and 6 deletions
+6
View File
@@ -57,6 +57,12 @@ jobs:
- name: Test (app) - name: Test (app)
working-directory: app working-directory: app
run: flutter test run: flutter test
- name: Dependencies (localsend_isolates)
working-directory: packages/localsend_isolates
run: flutter pub get
- name: Test (localsend_isolates)
working-directory: packages/localsend_isolates
run: flutter test
packaging: packaging:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -215,14 +215,14 @@ Future<(String, String?, String)> digestFilePathAndPrepareDirectory({
if (!p.isWithin(parentDirectory, dir)) { if (!p.isWithin(parentDirectory, dir)) {
throw 'Path traversal detected'; throw 'Path traversal detected';
} }
try {
Directory(dir).createSync(recursive: true);
} catch (e) {
_logger.warning('Could not create directory', e);
}
} }
// The destination directory may not exist anymore, e.g. because it was deleted
// or is on a drive that is no longer mounted. This also creates the
// sub-directories of a folder transfer. Errors are propagated so that the
// caller fails the upload instead of writing to a path that cannot be opened.
Directory(dir).createSync(recursive: true);
String destinationPath; String destinationPath;
int counter = 1; int counter = 1;
do { do {
+77
View File
@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.1" version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build: build:
dependency: transitive dependency: transitive
description: description:
@@ -113,6 +121,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.4" version: "2.0.4"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection: collection:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -161,6 +177,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.7" version: "3.1.7"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -214,6 +238,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -291,6 +320,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.12.0" version: "4.12.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
legalize: legalize:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -315,6 +368,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -575,6 +636,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.2" version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
type_plus: type_plus:
dependency: transitive dependency: transitive
description: description:
@@ -614,6 +683,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
+2
View File
@@ -33,4 +33,6 @@ dev_dependencies:
build_runner: 2.15.1 build_runner: 2.15.1
dart_mappable_builder: 4.8.0 dart_mappable_builder: 4.8.0
flutter_lints: 6.0.0 flutter_lints: 6.0.0
flutter_test:
sdk: flutter
freezed: 3.2.5 freezed: 3.2.5
@@ -0,0 +1,59 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:localsend_isolates/src/task/server/file_saver.dart';
import 'package:path/path.dart' as p;
void main() {
late Directory tempDir;
setUp(() {
tempDir = Directory.systemTemp.createTempSync('file_saver_test');
});
tearDown(() {
if (tempDir.existsSync()) {
tempDir.deleteSync(recursive: true);
}
});
Future<String> digest(String parentDirectory, String fileName) async {
final (path, _, _) = await digestFilePathAndPrepareDirectory(
parentDirectory: parentDirectory,
fileName: fileName,
createdDirectories: {},
);
return path;
}
test('creates the destination directory when it does not exist', () async {
final destination = p.join(tempDir.path, 'gone');
final path = await digest(destination, 'file.txt');
expect(Directory(destination).existsSync(), isTrue);
expect(path, p.join(destination, 'file.txt'));
});
test('creates the sub-directories of a folder transfer', () async {
final path = await digest(tempDir.path, p.join('outer', 'inner', 'file.txt'));
expect(Directory(p.join(tempDir.path, 'outer', 'inner')).existsSync(), isTrue);
expect(path, p.join(tempDir.path, 'outer', 'inner', 'file.txt'));
});
test('keeps an existing directory and its content', () async {
File(p.join(tempDir.path, 'file.txt')).writeAsStringSync('hello');
final path = await digest(tempDir.path, 'file.txt');
expect(path, p.join(tempDir.path, 'file (2).txt'));
expect(File(p.join(tempDir.path, 'file.txt')).readAsStringSync(), 'hello');
});
test('still rejects path traversal', () async {
await expectLater(
digest(tempDir.path, p.join('..', 'escaped', 'file.txt')),
throwsA('Path traversal detected'),
);
});
}