diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8f9f66c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +# Dependabot configuration file. +# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates +version: 2 + +enable-beta-ecosystems: true + +updates: + - package-ecosystem: "pub" + directory: "ftp" + schedule: + interval: "monthly" + - package-ecosystem: "pub" + directory: "ftp_client_io" + schedule: + interval: "monthly" + - package-ecosystem: "pub" + directory: "ftp_io" + schedule: + interval: "monthly" + - package-ecosystem: "pub" + directory: "ftp_server_io" + schedule: + interval: "monthly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/run_ci.yml b/.github/workflows/run_ci.yml new file mode 100644 index 0000000..7e20a06 --- /dev/null +++ b/.github/workflows/run_ci.yml @@ -0,0 +1,36 @@ +name: Run CI +on: + push: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # every sunday at midnight + +jobs: + test: + name: Test on ${{ matrix.os }} / ${{ matrix.dart }} + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: . + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + dart: stable + - os: ubuntu-latest + dart: beta + - os: ubuntu-latest + dart: dev + - os: windows-latest + dart: stable + - os: macos-latest + dart: stable + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1.4 + with: + sdk: ${{ matrix.dart }} + - run: dart --version + - run: dart pub global activate dev_build + - run: dart pub global run dev_build:run_ci --recursive diff --git a/.github/workflows/run_ci_downgrade_analyze.yml b/.github/workflows/run_ci_downgrade_analyze.yml new file mode 100644 index 0000000..141f478 --- /dev/null +++ b/.github/workflows/run_ci_downgrade_analyze.yml @@ -0,0 +1,29 @@ +name: Run CI Downgrade analyze +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # every sunday at midnight + +jobs: + test: + name: Test on ${{ matrix.os }} / dart ${{ matrix.dart }} + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: . + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + dart: stable + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1.4 + with: + sdk: ${{ matrix.dart }} + - run: dart --version + - run: dart pub global activate dev_build + - run: dart pub global run dev_build:run_ci --pub-downgrade --analyze --no-override --recursive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5e6ded --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.local/ +pubspec_overrides.yaml \ No newline at end of file diff --git a/README.md b/README.md index 9b1e73a..adcd278 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # ftp.dart -FTP abstraction, client, server + +FTP abstraction for client and server +- IO server implementation using [ftp_server](https://pub.dev/packages/ftp_server) +- IO client implementation using [ftpconnect](https://pub.dev/packages/ftpconnect) diff --git a/ftp/.gitignore b/ftp/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/ftp/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/ftp/README.md b/ftp/README.md new file mode 100644 index 0000000..1c545ef --- /dev/null +++ b/ftp/README.md @@ -0,0 +1,15 @@ +# ftp + +FTP abstract client + +## Setup + +In `pubspec.yaml`: + +```yaml + tekartik_ftp: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp +``` \ No newline at end of file diff --git a/ftp/analysis_options.yaml b/ftp/analysis_options.yaml new file mode 100644 index 0000000..b54503f --- /dev/null +++ b/ftp/analysis_options.yaml @@ -0,0 +1 @@ +include: package:tekartik_lints/package.yaml diff --git a/ftp/lib/ftp_client.dart b/ftp/lib/ftp_client.dart new file mode 100644 index 0000000..badc50b --- /dev/null +++ b/ftp/lib/ftp_client.dart @@ -0,0 +1 @@ +export 'src/client/ftp_client.dart' show FtpClient, FtpEntry, FtpEntryType; diff --git a/ftp/lib/ftp_server.dart b/ftp/lib/ftp_server.dart new file mode 100644 index 0000000..a8e099b --- /dev/null +++ b/ftp/lib/ftp_server.dart @@ -0,0 +1 @@ +export 'src/server/ftp_server.dart' show FtpServer; diff --git a/ftp/lib/src/client/ftp_client.dart b/ftp/lib/src/client/ftp_client.dart new file mode 100644 index 0000000..ed1806c --- /dev/null +++ b/ftp/lib/src/client/ftp_client.dart @@ -0,0 +1,55 @@ +import 'dart:io'; + +/// FTP client interface +abstract class FtpClient { + /// Connect + Future connect(); + + /// Disconnect + Future disconnect(); + + /// Change dir + Future cd(String path); + + /// List entries + Future> list(); + + /// Download file + Future downloadFile(String remoteName, File localFile); + + /// Update file + Future uploadFile( + File localFile, + String remoteName, + ); +} + +/// FTP entry +abstract class FtpEntry { + /// name of entry + String get name; + + /// type of entry + FtpEntryType get type; + + /// modified time, if available + DateTime? get modified; + + /// -1 if unknown, dir size should not be consided (sometimes reported as 4096) + int get size; +} + +/// FTP entry type +enum FtpEntryType { + /// File + file, + + /// Directory + dir, + + /// Link + link, + + /// Unknown + unknown +} diff --git a/ftp/lib/src/server/ftp_server.dart b/ftp/lib/src/server/ftp_server.dart new file mode 100644 index 0000000..3f2bbd9 --- /dev/null +++ b/ftp/lib/src/server/ftp_server.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +/// FTP server +abstract class FtpServer { + /// port + int get port; + + /// Root directory + Directory get root; + + /// Start the server + Future start(); + + /// Stop the server + Future stop(); +} diff --git a/ftp/pubspec.yaml b/ftp/pubspec.yaml new file mode 100644 index 0000000..b65b393 --- /dev/null +++ b/ftp/pubspec.yaml @@ -0,0 +1,19 @@ +name: tekartik_ftp +description: Abstract server and client ftp library +version: 1.0.0 + +environment: + sdk: ^3.5.0 + +# Add regular dependencies here. +dependencies: + # path: ^1.8.0 + +dev_dependencies: + lints: ">=5.0.0" + tekartik_lints: + git: + url: https://github.com/tekartik/common.dart + ref: dart3a + path: packages/lints + test: ">=1.24.0" diff --git a/ftp_client_io/.gitignore b/ftp_client_io/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/ftp_client_io/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/ftp_client_io/README.md b/ftp_client_io/README.md new file mode 100644 index 0000000..2b44318 --- /dev/null +++ b/ftp_client_io/README.md @@ -0,0 +1,15 @@ +# ftp_client_io + +FTP client io implementation + +## Setup + +In `pubspec.yaml`: + +```yaml + tekartik_ftp_client_io: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp_io +``` \ No newline at end of file diff --git a/ftp_client_io/analysis_options.yaml b/ftp_client_io/analysis_options.yaml new file mode 100644 index 0000000..b54503f --- /dev/null +++ b/ftp_client_io/analysis_options.yaml @@ -0,0 +1 @@ +include: package:tekartik_lints/package.yaml diff --git a/ftp_client_io/lib/ftp_client_io.dart b/ftp_client_io/lib/ftp_client_io.dart new file mode 100644 index 0000000..8ce05c6 --- /dev/null +++ b/ftp_client_io/lib/ftp_client_io.dart @@ -0,0 +1,2 @@ +export 'package:tekartik_ftp/ftp_client.dart'; +export 'src/ftp_client_ftpconnect.dart' show FtpClientIo; diff --git a/ftp_client_io/lib/src/ftp_client_ftpconnect.dart b/ftp_client_io/lib/src/ftp_client_ftpconnect.dart new file mode 100644 index 0000000..60ffc33 --- /dev/null +++ b/ftp_client_io/lib/src/ftp_client_ftpconnect.dart @@ -0,0 +1,127 @@ +import 'dart:io'; + +import 'package:ftpconnect/ftpconnect.dart' as fc; +import 'package:tekartik_common_utils/common_utils_import.dart'; +import 'package:tekartik_ftp/ftp_client.dart'; + +/// Allow debugging from external client +final debugFtpClientFtpConnect = false; +// final debugFtpClientFtpConnect = devWarning(true); + +bool get _debug => debugFtpClientFtpConnect; +void _log(Object? message) { + if (_debug) { + // ignore: avoid_print + print(message); + } +} + +/// Ftp client using io +abstract class FtpClientIo implements FtpClient { + /// Constructor + factory FtpClientIo( + {required String host, + required String user, + required String password, + int? port}) => + _FtpClientFtpConnect( + host: host, user: user, password: password, port: port); +} + +/// Ftp client using ftpconnect +class _FtpClientFtpConnect implements FtpClientIo { + late fc.FTPConnect _delegate; + + @override + Future connect() async { + return await _delegate.connect(); + } + + @override + Future disconnect() async { + return await _delegate.disconnect(); + } + + /// Constructor + _FtpClientFtpConnect( + {required String host, + required String user, + required String password, + int? port}) { + _delegate = fc.FTPConnect(host, + port: port, + user: user, + pass: password, + securityType: fc.SecurityType.FTP); + } + + @override + Future> list() async { + if (_debug) { + _log('list'); + } + var entries = await _delegate.listDirectoryContent(); + return entries.map((e) => _FtpEntry(e)).toList(); + } + + @override + Future downloadFile(String remoteName, File localFile) async { + if (_debug) { + _log('download $remoteName to $localFile'); + } + return await _delegate.downloadFile(remoteName, localFile); + } + + @override + Future cd(String path) async { + if (_debug) { + _log('cd $path'); + } + return await _delegate.changeDirectory(path); + } + + @override + Future uploadFile(File localFile, String remoteName) async { + if (_debug) { + _log('upload $localFile to $remoteName'); + } + return await _delegate.uploadFile(localFile, sRemoteName: remoteName); + } +} + +class _FtpEntry implements FtpEntry { + final fc.FTPEntry _delegate; + + _FtpEntry(this._delegate); + @override + String get name => _delegate.name; + + @override + FtpEntryType get type => _delegate.type.toFtpType(); + + @override + int get size => _delegate.size ?? -1; + + @override + DateTime? get modified => _delegate.modifyTime; + + @override + String toString() => + 'FtpEntry(name: $name, type: $type${size > 0 ? ', size: $size' : ''}' + '${modified != null ? ', ${modified?.toIso8601String()}' : ''}'; +} + +extension on fc.FTPEntryType { + FtpEntryType toFtpType() { + switch (this) { + case fc.FTPEntryType.FILE: + return FtpEntryType.file; + case fc.FTPEntryType.DIR: + return FtpEntryType.dir; + case fc.FTPEntryType.LINK: + return FtpEntryType.link; + default: + return FtpEntryType.unknown; + } + } +} diff --git a/ftp_client_io/pubspec.yaml b/ftp_client_io/pubspec.yaml new file mode 100644 index 0000000..fba18e6 --- /dev/null +++ b/ftp_client_io/pubspec.yaml @@ -0,0 +1,33 @@ +name: tekartik_ftp_client_io +description: IO client implementation +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.5.0 + +# Add regular dependencies here. +dependencies: + tekartik_ftp: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp + ftpconnect: ">=2.0.7" + tekartik_common_utils: + git: + url: https://github.com/tekartik/common_utils.dart + ref: dart3a + +dev_dependencies: + lints: ">=5.0.0" + tekartik_lints: + git: + url: https://github.com/tekartik/common.dart + ref: dart3a + path: packages/lints + test: ">=1.24.0" + +dependency_overrides: + tekartik_ftp: + path: ../ftp \ No newline at end of file diff --git a/ftp_io/.gitignore b/ftp_io/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/ftp_io/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/ftp_io/README.md b/ftp_io/README.md new file mode 100644 index 0000000..33bc958 --- /dev/null +++ b/ftp_io/README.md @@ -0,0 +1,15 @@ +# ftp_server_io + +FTP server io implementation + +## Setup + +In `pubspec.yaml`: + +```yaml + tekartik_ftp_server_io: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp_server_io +``` \ No newline at end of file diff --git a/ftp_io/analysis_options.yaml b/ftp_io/analysis_options.yaml new file mode 100644 index 0000000..b54503f --- /dev/null +++ b/ftp_io/analysis_options.yaml @@ -0,0 +1 @@ +include: package:tekartik_lints/package.yaml diff --git a/ftp_io/lib/ftp_client_io.dart b/ftp_io/lib/ftp_client_io.dart new file mode 100644 index 0000000..70f562f --- /dev/null +++ b/ftp_io/lib/ftp_client_io.dart @@ -0,0 +1 @@ +export 'package:tekartik_ftp_client_io/ftp_client_io.dart'; diff --git a/ftp_io/lib/ftp_server_io.dart b/ftp_io/lib/ftp_server_io.dart new file mode 100644 index 0000000..ecfe152 --- /dev/null +++ b/ftp_io/lib/ftp_server_io.dart @@ -0,0 +1 @@ +export 'package:tekartik_ftp_server_io/ftp_server_io.dart'; diff --git a/ftp_io/pubspec.yaml b/ftp_io/pubspec.yaml new file mode 100644 index 0000000..14ce5a4 --- /dev/null +++ b/ftp_io/pubspec.yaml @@ -0,0 +1,43 @@ +name: tekartik_ftp_io +description: Server and client io implementation +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.5.0 + +# Add regular dependencies here. +dependencies: + fs_shim: ">=2.3.3+1" + path: ">=1.9.0" + tekartik_ftp: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp + tekartik_ftp_client_io: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp_io + tekartik_ftp_server_io: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp_server_io +dev_dependencies: + lints: ">=5.0.0" + tekartik_lints: + git: + url: https://github.com/tekartik/common.dart + ref: dart3a + path: packages/lints + test: ">=1.24.0" + +dependency_overrides: + tekartik_ftp: + path: ../ftp + tekartik_ftp_client_io: + path: ../ftp_client_io + tekartik_ftp_server_io: + path: ../ftp_server_io diff --git a/ftp_io/test/ftp_io_test.dart b/ftp_io/test/ftp_io_test.dart new file mode 100644 index 0000000..3f9ca19 --- /dev/null +++ b/ftp_io/test/ftp_io_test.dart @@ -0,0 +1,147 @@ +@TestOn('vm') +library; + +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:fs_shim/utils/io/read_write.dart'; + +import 'package:path/path.dart'; +import 'package:tekartik_ftp_io/ftp_client_io.dart'; +import 'package:tekartik_ftp_io/ftp_server_io.dart'; +import 'package:test/test.dart'; + +var port = 7079; +var globalLocalDir = join('.local', 'ftp', 'local'); +var globalServerDir = join('.local', 'ftp', 'server'); +Future main() async { + group('fs_ftp', () { + late FtpServer server; + late FtpClient client; + + setUpAll(() async { + await Directory(globalServerDir).emptyOrCreate(); + server = FtpServerIo( + port: port, + username: 'admin', + password: 'admin', + root: Directory(globalServerDir), + ); + + await server.start(); + + client = FtpClientIo( + host: 'localhost', user: 'admin', password: 'admin', port: port); + await client.connect(); + }); + tearDownAll(() async { + await client.disconnect(); + }); + + Future enterDir(String dirname) async { + expect(await client.cd(dirname), isTrue, reason: 'cd $dirname'); + } + + /// Returns the local directory created + Future emptyServerAndEnterDir(String dirname) async { + var serverDir = Directory(join(globalServerDir, dirname)); + await serverDir.emptyOrCreate(); + await enterDir(dirname); + return serverDir; + } + + Future leaveDir() async { + expect(await client.cd('..'), isTrue); + } + + test('cd', () async { + var dirname = 'cd'; + await emptyServerAndEnterDir(dirname); + try { + expect(await client.cd('..'), isTrue); + expect(await client.cd(dirname), isTrue); + } finally { + await leaveDir(); + } + }); + test('list', () async { + var dirname = 'list'; + var serverDir = await emptyServerAndEnterDir(dirname); + try { + var entries = await client.list(); + expect(entries, isEmpty); + await File(join(serverDir.path, 'test.txt')) + .writeAsString('some_content'); + entries = await client.list(); + expect(entries.length, 1); + var file = entries.first; + expect(file.name, 'test.txt'); + expect(file.size, 12); + expect(file.type, FtpEntryType.file); + + await Directory(join(serverDir.path, 'sub')).create(recursive: true); + entries = await client.list(); + expect(entries.length, 2); + var entryDir = + entries.where((entry) => entry.type == FtpEntryType.dir).first; + expect(entryDir.name, 'sub'); + } finally { + await leaveDir(); + } + }); + test('download', () async { + var dirname = 'download'; + var serverDir = await emptyServerAndEnterDir(dirname); + try { + var localDir = Directory(join(globalLocalDir, dirname)); + await localDir.emptyOrCreate(); + + var text = 'some_content_${DateTime.now().toIso8601String()}'; + + await File(join(serverDir.path, 'test.txt')).writeAsString(text); + var downloadFile = File(join(localDir.path, 'test.txt')); + await client.downloadFile('test.txt', downloadFile); + expect(await downloadFile.readAsString(), text); + + /// Binary + var bytes = Uint8List.fromList([1, 2, 3]); + await File(join(serverDir.path, 'test.bin')).writeAsBytes(bytes); + downloadFile = File(join(localDir.path, 'test.bin')); + await client.downloadFile('test.bin', downloadFile); + expect(await downloadFile.readAsBytes(), bytes); + } finally { + await leaveDir(); + } + }); + test('upload', () async { + var dirname = 'upload'; + await emptyServerAndEnterDir(dirname); + try { + var localDir = Directory(join(globalLocalDir, dirname)); + await localDir.emptyOrCreate(); + + var text = 'some_content_${DateTime.now().toIso8601String()}'; + + var sourceLocalFile = File(join(localDir.path, 'src_test.txt')); + await sourceLocalFile.writeAsString(text); + await client.uploadFile(sourceLocalFile, 'test.txt'); + + var downloadFile = File(join(localDir.path, 'test.txt')); + await client.downloadFile('test.txt', downloadFile); + expect(await downloadFile.readAsString(), text); + + /// Binary + var bytes = Uint8List.fromList([1, 2, 3]); + sourceLocalFile = File(join(localDir.path, 'src_test.bin')); + await sourceLocalFile.writeAsBytes(bytes); + await client.uploadFile(sourceLocalFile, 'test.bin'); + + downloadFile = File(join(localDir.path, 'test.bin')); + await client.downloadFile('test.bin', downloadFile); + expect(await downloadFile.readAsBytes(), bytes); + } finally { + await leaveDir(); + } + }); + }); +} diff --git a/ftp_server_io/.gitignore b/ftp_server_io/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/ftp_server_io/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/ftp_server_io/README.md b/ftp_server_io/README.md new file mode 100644 index 0000000..072ea17 --- /dev/null +++ b/ftp_server_io/README.md @@ -0,0 +1,15 @@ +# ftp__io + +FTP cliand and server io implementation + +## Setup + +In `pubspec.yaml`: + +```yaml + tekartik_ftp_server_io: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp_server_io +``` \ No newline at end of file diff --git a/ftp_server_io/analysis_options.yaml b/ftp_server_io/analysis_options.yaml new file mode 100644 index 0000000..b54503f --- /dev/null +++ b/ftp_server_io/analysis_options.yaml @@ -0,0 +1 @@ +include: package:tekartik_lints/package.yaml diff --git a/ftp_server_io/lib/ftp_server_io.dart b/ftp_server_io/lib/ftp_server_io.dart new file mode 100644 index 0000000..a157709 --- /dev/null +++ b/ftp_server_io/lib/ftp_server_io.dart @@ -0,0 +1,2 @@ +export 'package:tekartik_ftp/ftp_server.dart'; +export 'src/ftp_server_io.dart' show FtpServerIo; diff --git a/ftp_server_io/lib/src/ftp_server_io.dart b/ftp_server_io/lib/src/ftp_server_io.dart new file mode 100644 index 0000000..923d8f7 --- /dev/null +++ b/ftp_server_io/lib/src/ftp_server_io.dart @@ -0,0 +1,50 @@ +import 'dart:io'; + +import 'package:ftp_server/ftp_server.dart' as impl; +import 'package:ftp_server/server_type.dart' as impl; +import 'package:path/path.dart'; +import 'package:tekartik_ftp/ftp_server.dart'; + +/// Ftp server io implementation +abstract class FtpServerIo implements FtpServer { + /// Create a new instance + factory FtpServerIo( + {required int port, + required Directory root, + String? username, + String? password}) { + return _FtpServerIoImpl(port: port, root: root); + } +} + +class _FtpServerIoImpl implements FtpServerIo { + @override + final int port; + @override + final Directory root; + late final impl.FtpServer _delegate; + + _FtpServerIoImpl( + {required this.port, + required this.root, + String? username, + String? password}) { + _delegate = impl.FtpServer( + port, + username: username, + password: password, + sharedDirectories: [root.path], + startingDirectory: basename(root.path), + serverType: impl.ServerType.readAndWrite, // or ServerType.readOnly + ); + } + @override + Future start() async { + await _delegate.startInBackground(); + } + + @override + Future stop() async { + await _delegate.stop(); + } +} diff --git a/ftp_server_io/pubspec.yaml b/ftp_server_io/pubspec.yaml new file mode 100644 index 0000000..ee42c36 --- /dev/null +++ b/ftp_server_io/pubspec.yaml @@ -0,0 +1,29 @@ +name: tekartik_ftp_server_io +description: Server io implementation +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.5.0 + +# Add regular dependencies here. +dependencies: + tekartik_ftp: + git: + url: https://github.com/tekartik/ftp.dart + ref: dart3a + path: ftp + ftp_server: ">=1.0.4" + path: ">=1.9.0" +dev_dependencies: + lints: ">=5.0.0" + tekartik_lints: + git: + url: https://github.com/tekartik/common.dart + ref: dart3a + path: packages/lints + test: ">=1.24.0" + +dependency_overrides: + tekartik_ftp: + path: ../ftp