├── test ├── fixtures │ ├── package_0 │ │ ├── 0.0.1 │ │ │ ├── LICENSE │ │ │ ├── CHANGELOG.md │ │ │ ├── README.md │ │ │ └── pubspec.yaml │ │ ├── 0.0.2 │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── CHANGELOG.md │ │ │ └── pubspec.yaml │ │ ├── 0.0.3 │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── CHANGELOG.md │ │ │ └── pubspec.yaml │ │ ├── 1.0.0 │ │ │ ├── LICENSE │ │ │ └── pubspec.yaml │ │ ├── 0.0.3+1 │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── CHANGELOG.md │ │ │ └── pubspec.yaml │ │ └── 1.0.0-noreadme │ │ │ ├── LICENSE │ │ │ └── pubspec.yaml │ ├── package_1 │ │ └── 0.0.1 │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── pubspec.yaml │ └── file_store │ │ └── .gitignore ├── file_store_test.dart ├── utils.dart └── unpub_test.dart ├── .idea ├── .gitignore ├── misc.xml ├── kotlinc.xml ├── modules.xml └── unpub-2.0.0.iml ├── assets ├── 1.png ├── 2.png └── 3.png ├── lib ├── unpub_api │ ├── pubspec.yaml │ └── lib │ │ ├── models.dart │ │ └── models.g.dart ├── unpub.dart └── src │ ├── package_store.dart │ ├── meta_store.dart │ ├── utils.dart │ ├── file_store.dart │ ├── app.g.dart │ ├── models.dart │ ├── models.g.dart │ ├── mongo_store.dart │ ├── app.dart │ └── static │ └── index.html.dart ├── Dockerfile ├── .gitignore ├── docker-compose.yml ├── tool └── pre_publish.dart ├── example └── main.dart ├── CHANGELOG.md ├── pubspec.yaml ├── LICENSE ├── bin └── unpub.dart └── README.md /test/fixtures/package_0/0.0.1/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.2/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/package_0/1.0.0/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/package_1/0.0.1/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3+1/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/package_1/0.0.1/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/package_0/1.0.0-noreadme/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/file_store/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.1/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.1 2 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.1/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.1 2 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.2/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.2 2 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.3 2 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3+1/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.3+1 2 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.2/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.2 2 | 3 | # 0.0.1 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeqinjie/unpub-2.0.0-docker/HEAD/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeqinjie/unpub-2.0.0-docker/HEAD/assets/2.png -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeqinjie/unpub-2.0.0-docker/HEAD/assets/3.png -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.3 2 | 3 | # 0.0.2 4 | 5 | # 0.0.1 6 | -------------------------------------------------------------------------------- /lib/unpub_api/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: unpub_api 2 | 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3+1/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.3+1 2 | 3 | # 0.0.3 4 | 5 | # 0.0.2 6 | 7 | # 0.0.1 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | from dart 2 | 3 | WORKDIR /app 4 | COPY . ./ 5 | RUN dart pub get 6 | ENTRYPOINT ["dart", "run", "bin/unpub.dart", "--database", "mongodb://mongo:27017/dart_pub"] -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.1/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: package_0 2 | description: test 3 | version: 0.0.1 4 | homepage: https://example.com 5 | environment: 6 | sdk: ">=2.0.0 <3.0.0" 7 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.2/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: package_0 2 | description: test 3 | version: 0.0.2 4 | homepage: https://example.com 5 | environment: 6 | sdk: ">=2.0.0 <3.0.0" 7 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: package_0 2 | description: test 3 | version: 0.0.3 4 | homepage: https://example.com 5 | environment: 6 | sdk: ">=2.0.0 <3.0.0" 7 | -------------------------------------------------------------------------------- /test/fixtures/package_0/1.0.0/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: package_0 2 | description: test 3 | version: 1.0.0 4 | homepage: https://example.com 5 | environment: 6 | sdk: ">=2.0.0 <3.0.0" 7 | -------------------------------------------------------------------------------- /test/fixtures/package_1/0.0.1/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: package_1 2 | description: test 3 | version: 0.0.1 4 | homepage: https://example.com 5 | environment: 6 | sdk: ">=2.0.0 <3.0.0" 7 | -------------------------------------------------------------------------------- /test/fixtures/package_0/0.0.3+1/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: package_0 2 | description: test 3 | version: 0.0.3+1 4 | homepage: https://example.com 5 | environment: 6 | sdk: ">=2.0.0 <3.0.0" 7 | -------------------------------------------------------------------------------- /test/fixtures/package_0/1.0.0-noreadme/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: package_0 2 | description: test 3 | version: 1.0.0-noreadme 4 | homepage: https://example.com 5 | environment: 6 | sdk: ">=2.0.0 <3.0.0" 7 | -------------------------------------------------------------------------------- /lib/unpub.dart: -------------------------------------------------------------------------------- 1 | export 'src/meta_store.dart'; 2 | export 'src/mongo_store.dart'; 3 | export 'src/package_store.dart'; 4 | export 'src/file_store.dart'; 5 | export 'src/app.dart'; 6 | export 'src/models.dart'; 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | unpub-packages/ 13 | .vscode/ 14 | -------------------------------------------------------------------------------- /lib/src/package_store.dart: -------------------------------------------------------------------------------- 1 | abstract class PackageStore { 2 | bool supportsDownloadUrl = false; 3 | 4 | String downloadUrl(String name, String version) { 5 | throw 'downloadUri not implemented'; 6 | } 7 | 8 | Stream> download(String name, String version) { 9 | throw 'download not implemented'; 10 | } 11 | 12 | Future upload(String name, String version, List content); 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | unpub: 4 | build: ./ 5 | container_name: unpub 6 | restart: always 7 | ports: 8 | - 4000:4000 9 | volumes: 10 | - ~/.unpub-packages:/app/unpub-packages 11 | depends_on: 12 | - mongo 13 | mongo: 14 | image: mongo:4.2.19 15 | container_name: unpub_mongo 16 | restart: always 17 | volumes: 18 | - ~/.unpub_mongo:/data/db -------------------------------------------------------------------------------- /tool/pre_publish.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart' as path; 3 | 4 | var files = ['index.html', 'main.dart.js']; 5 | 6 | main(List args) { 7 | for (var file in files) { 8 | var content = 9 | File(path.absolute('unpub_web/build', file)).readAsStringSync(); 10 | content = content.replaceAll('\\', '\\\\').replaceAll('\$', '\\\$'); 11 | content = 'const content="""$content""";'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart/mongo_dart.dart'; 2 | import 'package:unpub/unpub.dart' as unpub; 3 | 4 | main(List args) async { 5 | final db = Db('mongodb://localhost:27017/dart_pub'); 6 | await db.open(); // make sure the MongoDB connection opened 7 | 8 | final app = unpub.App( 9 | metaStore: unpub.MongoStore(db), 10 | packageStore: unpub.FileStore('./unpub-packages'), 11 | ); 12 | 13 | final server = await app.serve('0.0.0.0', 4000); 14 | print('Serving at http://${server.address.host}:${server.port}'); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/meta_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:unpub/src/models.dart'; 2 | 3 | abstract class MetaStore { 4 | Future queryPackage(String name); 5 | 6 | Future addVersion(String name, UnpubVersion version); 7 | 8 | Future addUploader(String name, String email); 9 | 10 | Future removeUploader(String name, String email); 11 | 12 | void increaseDownloads(String name, String version); 13 | 14 | Future queryPackages({ 15 | required int size, 16 | required int page, 17 | required String sort, 18 | String? keyword, 19 | String? uploader, 20 | String? dependency, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | 3 | - Supports NNBD 4 | - Fixes Web styles 5 | 6 | ## 1.2.1 7 | 8 | ## 1.2.0 9 | 10 | - Supports mongodb pool connection 11 | - Update web page styles 12 | 13 | ## 1.1.0 14 | 15 | - Add badges for version and downloads 16 | - Fix web page styles 17 | 18 | ## 1.0.0 19 | 20 | ## 0.4.0 21 | 22 | ## 0.3.0 23 | 24 | ## 0.2.2 25 | 26 | ## 0.2.1 27 | 28 | ## 0.2.0 29 | 30 | - Refactor 31 | - Semver whitelist 32 | 33 | ## 0.1.1 34 | 35 | - Get email via Google APIs 36 | - Upload validator 37 | 38 | ## 0.1.0 39 | 40 | - `pub get` 41 | - `pub publish` with permission check 42 | 43 | ## 0.0.1 44 | 45 | - Initial version, created by Stagehand 46 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaml/yaml.dart'; 2 | 3 | convertYaml(dynamic value) { 4 | if (value is YamlMap) { 5 | return value 6 | .cast() 7 | .map((k, v) => MapEntry(k, convertYaml(v))); 8 | } 9 | if (value is YamlList) { 10 | return value.map((e) => convertYaml(e)).toList(); 11 | } 12 | return value; 13 | } 14 | 15 | Map? loadYamlAsMap(dynamic value) { 16 | var yamlMap = loadYaml(value) as YamlMap?; 17 | return convertYaml(yamlMap).cast(); 18 | } 19 | 20 | List getPackageTags(Map pubspec) { 21 | // TODO: web and other tags 22 | if (pubspec['flutter'] != null) { 23 | return ['flutter']; 24 | } else { 25 | return ['flutter', 'web', 'other']; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: unpub 2 | description: Self-hosted private Dart Pub server for Enterprise, with a simple web interface to search and view packages information. 3 | version: 2.0.0 4 | homepage: https://github.com/bytedance/unpub 5 | environment: 6 | sdk: ">=2.12.0 <3.0.0" 7 | executables: 8 | unpub: unpub 9 | dependencies: 10 | args: ^2.2.0 11 | shelf: ^1.2.0 12 | shelf_router: ^1.1.1 13 | http_parser: ^4.0.0 14 | http: ^0.13.3 15 | archive: ^3.1.2 16 | logging: ^1.0.1 17 | meta: ^1.1.7 18 | googleapis: ^4.0.0 19 | yaml: ^3.1.0 20 | pub_semver: ^2.0.0 21 | json_annotation: ^4.1.0 22 | mongo_dart: ^0.7.1 23 | mime: ^1.0.0 24 | intl: ^0.17.0 25 | path: ^1.6.2 26 | collection: ^1.15.0 27 | shelf_cors_headers: ^0.1.2 28 | dev_dependencies: 29 | test: ^1.6.1 30 | build_runner: ^2.1.1 31 | json_serializable: ^5.0.0 32 | shelf_router_generator: ^1.0.1 33 | chunked_stream: ^1.4.1 34 | -------------------------------------------------------------------------------- /lib/src/file_store.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart' as path; 3 | import 'package_store.dart'; 4 | 5 | class FileStore extends PackageStore { 6 | String baseDir; 7 | String Function(String name, String version)? getFilePath; 8 | 9 | FileStore(this.baseDir, {this.getFilePath}); 10 | 11 | File _getTarballFile(String name, String version) { 12 | final filePath = 13 | getFilePath?.call(name, version) ?? '$name-$version.tar.gz'; 14 | return File(path.join(baseDir, filePath)); 15 | } 16 | 17 | @override 18 | Future upload(String name, String version, List content) async { 19 | var file = _getTarballFile(name, version); 20 | await file.create(recursive: true); 21 | await file.writeAsBytes(content); 22 | } 23 | 24 | @override 25 | Stream> download(String name, String version) { 26 | return _getTarballFile(name, version).openRead(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rongjian Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/unpub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart' as path; 3 | import 'package:args/args.dart'; 4 | import 'package:mongo_dart/mongo_dart.dart'; 5 | import 'package:unpub/unpub.dart' as unpub; 6 | 7 | main(List args) async { 8 | var parser = ArgParser(); 9 | parser.addOption('host', abbr: 'h', defaultsTo: '0.0.0.0'); 10 | parser.addOption('port', abbr: 'p', defaultsTo: '4000'); 11 | parser.addOption('database', abbr: 'd', defaultsTo: 'mongodb://localhost:27017/dart_pub'); 12 | 13 | var results = parser.parse(args); 14 | 15 | var host = results['host'] as String?; 16 | var port = int.parse(results['port'] as String); 17 | var dbUri = results['database'] as String; 18 | 19 | if (results.rest.isNotEmpty) { 20 | print('Got unexpected arguments: "${results.rest.join(' ')}".\n\nUsage:\n'); 21 | print(parser.usage); 22 | exit(1); 23 | } 24 | 25 | final db = Db(dbUri); 26 | await db.open(); 27 | 28 | var baseDir = path.absolute('unpub-packages'); 29 | 30 | var app = unpub.App( 31 | metaStore: unpub.MongoStore(db), 32 | packageStore: unpub.FileStore(baseDir), 33 | ); 34 | 35 | var server = await app.serve(host, port); 36 | print('Serving at http://${server.address.host}:${server.port}'); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/app.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app.dart'; 4 | 5 | // ************************************************************************** 6 | // ShelfRouterGenerator 7 | // ************************************************************************** 8 | 9 | Router _$AppRouter(App service) { 10 | final router = Router(); 11 | router.add('GET', r'/api/packages/', service.getVersions); 12 | router.add( 13 | 'GET', r'/api/packages//versions/', service.getVersion); 14 | router.add( 15 | 'GET', r'/packages//versions/.tar.gz', service.download); 16 | router.add('GET', r'/api/packages/versions/new', service.getUploadUrl); 17 | router.add('POST', r'/api/packages/versions/newUpload', service.upload); 18 | router.add( 19 | 'GET', r'/api/packages/versions/newUploadFinish', service.uploadFinish); 20 | router.add('POST', r'/api/packages//uploaders', service.addUploader); 21 | router.add('DELETE', r'/api/packages//uploaders/', 22 | service.removeUploader); 23 | router.add('GET', r'/webapi/packages', service.getPackages); 24 | router.add( 25 | 'GET', r'/webapi/package//', service.getPackageDetail); 26 | router.add('GET', r'/', service.indexHtml); 27 | router.add('GET', r'/packages', service.indexHtml); 28 | router.add('GET', r'/packages/', service.indexHtml); 29 | router.add('GET', r'/packages//versions/', service.indexHtml); 30 | router.add('GET', r'/main.dart.js', service.mainDartJs); 31 | router.add('GET', r'/badge//', service.badge); 32 | return router; 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 通过 Docker 部署 4 | 为了方便大家移植部署,这边将 `unpub` 打包成 `docker` 镜像环境 5 | 6 | ### 安装镜像 7 | 首先拉取 GitHub 地址 代码,安装 [docker](https://www.docker.com/get-started/) 环境, 然后在项目根目录下执行下面命令即可 8 | 9 | ```sh 10 | # install docker and cd docker-compose.yml file 11 | docker compose up -d 12 | ``` 13 | 14 | ### 安装运行成功如下 15 | 16 | 安装完成 17 | 18 | ![图片](https://github.com/zeqinjie/unpub-2.0.0-docker/blob/main/assets/1.png) 19 | 20 | 通过 docker ps -a 命令查看运行中容器 21 | 22 | ![图片](https://github.com/zeqinjie/unpub-2.0.0-docker/blob/main/assets/2.png) 23 | 24 | ![图片](https://github.com/zeqinjie/unpub-2.0.0-docker/blob/main/assets/3.png) 25 | 26 | ## bytedance Unpub 27 | ### Command Line 28 | 29 | ```sh 30 | pub global activate unpub 31 | unpub --database mongodb://localhost:27017/dart_pub # Replace this with production database uri 32 | ``` 33 | 34 | ### Dart API 35 | 36 | ```dart 37 | import 'package:mongo_dart/mongo_dart.dart'; 38 | import 'package:unpub/unpub.dart' as unpub; 39 | 40 | main(List args) async { 41 | final db = Db('mongodb://localhost:27017/dart_pub'); 42 | await db.open(); // make sure the MongoDB connection opened 43 | 44 | final app = unpub.App( 45 | metaStore: unpub.MongoStore(db), 46 | packageStore: unpub.FileStore('./unpub-packages'), 47 | ); 48 | 49 | final server = await app.serve('0.0.0.0', 4000); 50 | print('Serving at http://${server.address.host}:${server.port}'); 51 | } 52 | ``` 53 | 54 | ### 相关文章 55 | [Flutter搭建私有Pub仓库Docker部署](https://zhengzeqin.netlify.app/2022/05/16/flutter%E6%90%AD%E5%BB%BA%E7%A7%81%E6%9C%89pub%E4%BB%93%E5%BA%93docker%E9%83%A8%E7%BD%B2/) 56 | 57 | ### 具体了解地址 [unpub](https://github.com/bytedance/unpub) 58 | 59 | 60 | ## License 61 | 62 | MIT 63 | -------------------------------------------------------------------------------- /lib/src/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'models.g.dart'; 4 | 5 | DateTime identity(DateTime x) => x; 6 | 7 | @JsonSerializable(includeIfNull: false) 8 | class UnpubVersion { 9 | final String version; 10 | final Map pubspec; 11 | final String pubspecYaml; 12 | final String? uploader; // TODO: not sure why null. keep it nullable 13 | final String? readme; 14 | final String? changelog; 15 | 16 | @JsonKey(fromJson: identity, toJson: identity) 17 | final DateTime createdAt; 18 | 19 | UnpubVersion( 20 | this.version, 21 | this.pubspec, 22 | this.pubspecYaml, 23 | this.uploader, 24 | this.readme, 25 | this.changelog, 26 | this.createdAt, 27 | ); 28 | 29 | factory UnpubVersion.fromJson(Map map) => 30 | _$UnpubVersionFromJson(map); 31 | 32 | Map toJson() => _$UnpubVersionToJson(this); 33 | } 34 | 35 | @JsonSerializable() 36 | class UnpubPackage { 37 | final String name; 38 | final List versions; 39 | final bool private; 40 | final List uploaders; 41 | 42 | @JsonKey(fromJson: identity, toJson: identity) 43 | final DateTime createdAt; 44 | 45 | @JsonKey(fromJson: identity, toJson: identity) 46 | final DateTime updatedAt; 47 | 48 | final int? download; 49 | 50 | UnpubPackage( 51 | this.name, 52 | this.versions, 53 | this.private, 54 | this.uploaders, 55 | this.createdAt, 56 | this.updatedAt, 57 | this.download, 58 | ); 59 | 60 | factory UnpubPackage.fromJson(Map map) => 61 | _$UnpubPackageFromJson(map); 62 | } 63 | 64 | @JsonSerializable() 65 | class UnpubQueryResult { 66 | int count; 67 | List packages; 68 | 69 | UnpubQueryResult(this.count, this.packages); 70 | 71 | factory UnpubQueryResult.fromJson(Map map) => 72 | _$UnpubQueryResultFromJson(map); 73 | } 74 | -------------------------------------------------------------------------------- /test/file_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:chunked_stream/chunked_stream.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:test/test.dart'; 6 | import 'package:unpub/unpub.dart' as unpub; 7 | 8 | //test gzip data 9 | const TEST_PKG_DATA = [ 10 | 0x8b, 0x1f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, // 11 | 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // 12 | ]; 13 | 14 | main() { 15 | test('upload-download-default-path', () async { 16 | var baseDir = _setup_fixture('upload-download-default-path'); 17 | var store = unpub.FileStore(baseDir.path); 18 | await store.upload('test_package', '1.0.0', TEST_PKG_DATA); 19 | var pkg2 = await readByteStream(store.download('test_package', '1.0.0')); 20 | expect(pkg2, TEST_PKG_DATA); 21 | expect( 22 | File(path.join(baseDir.path, 'test_package-1.0.0.tar.gz')).existsSync(), 23 | isTrue); 24 | }); 25 | 26 | test('upload-download-custom-path', () async { 27 | var baseDir = _setup_fixture('upload-download-custom-path'); 28 | var store = unpub.FileStore(baseDir.path, getFilePath: newFilePathFunc()); 29 | await store.upload('test_package', '1.0.0', TEST_PKG_DATA); 30 | var pkg2 = await readByteStream(store.download('test_package', '1.0.0')); 31 | expect(pkg2, TEST_PKG_DATA); 32 | expect( 33 | File(path.join(baseDir.path, 'packages', 't', 'te', 'test_package', 34 | 'versions', 'test_package-1.0.0.tar.gz')) 35 | .existsSync(), 36 | isTrue); 37 | }); 38 | } 39 | 40 | String Function(String, String) newFilePathFunc() { 41 | return (String package, String version) { 42 | var grp = package[0]; 43 | var subgrp = package.substring(0, 2); 44 | return path.join('packages', grp, subgrp, package, 'versions', 45 | '$package-$version.tar.gz'); 46 | }; 47 | } 48 | 49 | _setup_fixture(final String name) { 50 | var baseDir = 51 | Directory(path.absolute('test', 'fixtures', 'file_store', name)); 52 | if (baseDir.existsSync()) { 53 | baseDir.deleteSync(recursive: true); 54 | } 55 | baseDir.createSync(); 56 | return baseDir; 57 | } 58 | -------------------------------------------------------------------------------- /test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart' as path; 3 | import 'package:http/http.dart' as http; 4 | import 'package:unpub/unpub.dart' as unpub; 5 | import 'package:mongo_dart/mongo_dart.dart'; 6 | 7 | final notExistingPacakge = 'not_existing_package'; 8 | final baseDir = path.absolute('unpub-packages'); 9 | final pubHostedUrl = 'http://localhost:4000'; 10 | final baseUri = Uri.parse(pubHostedUrl); 11 | 12 | final package0 = 'package_0'; 13 | final package1 = 'package_1'; 14 | final email0 = 'email0@example.com'; 15 | final email1 = 'email1@example.com'; 16 | final email2 = 'email2@example.com'; 17 | final email3 = 'email3@example.com'; 18 | 19 | createServer(String opEmail) async { 20 | final db = Db('mongodb://localhost:27017/dart_pub_test'); 21 | await db.open(); 22 | var mongoStore = unpub.MongoStore(db); 23 | 24 | var app = unpub.App( 25 | metaStore: mongoStore, 26 | packageStore: unpub.FileStore(baseDir), 27 | overrideUploaderEmail: opEmail, 28 | ); 29 | 30 | var server = await app.serve('0.0.0.0', 4000); 31 | return server; 32 | } 33 | 34 | Future getVersions(String package) { 35 | package = Uri.encodeComponent(package); 36 | return http.get(baseUri.resolve('/api/packages/$package')); 37 | } 38 | 39 | Future getSpecificVersion(String package, String version) { 40 | package = Uri.encodeComponent(package); 41 | version = Uri.encodeComponent(version); 42 | return http.get(baseUri.resolve('/api/packages/$package/versions/$version')); 43 | } 44 | 45 | Future pubPublish(String name, String version) { 46 | return Process.run('dart', ['pub', 'publish', '--force'], 47 | workingDirectory: path.absolute('test/fixtures', name, version), 48 | environment: {'PUB_HOSTED_URL': pubHostedUrl}); 49 | } 50 | 51 | Future pubUploader(String name, String operation, String email) { 52 | assert(['add', 'remove'].contains(operation), 'operation error'); 53 | 54 | return Process.run('dart', ['pub', 'uploader', operation, email], 55 | workingDirectory: path.absolute('test/fixtures', name, '0.0.1'), 56 | environment: {'PUB_HOSTED_URL': pubHostedUrl}); 57 | } 58 | -------------------------------------------------------------------------------- /lib/unpub_api/lib/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'models.g.dart'; 4 | 5 | @JsonSerializable() 6 | class ListApi { 7 | int count; 8 | List packages; 9 | 10 | ListApi(this.count, this.packages); 11 | 12 | factory ListApi.fromJson(Map map) => _$ListApiFromJson(map); 13 | Map toJson() => _$ListApiToJson(this); 14 | } 15 | 16 | @JsonSerializable() 17 | class ListApiPackage { 18 | String name; 19 | String? description; 20 | List tags; 21 | String latest; 22 | DateTime updatedAt; 23 | 24 | ListApiPackage( 25 | this.name, this.description, this.tags, this.latest, this.updatedAt); 26 | 27 | factory ListApiPackage.fromJson(Map map) => 28 | _$ListApiPackageFromJson(map); 29 | Map toJson() => _$ListApiPackageToJson(this); 30 | } 31 | 32 | @JsonSerializable() 33 | class DetailViewVersion { 34 | String version; 35 | DateTime createdAt; 36 | 37 | DetailViewVersion(this.version, this.createdAt); 38 | 39 | factory DetailViewVersion.fromJson(Map map) => 40 | _$DetailViewVersionFromJson(map); 41 | 42 | Map toJson() => _$DetailViewVersionToJson(this); 43 | } 44 | 45 | @JsonSerializable() 46 | class WebapiDetailView { 47 | String name; 48 | String version; 49 | String description; 50 | String homepage; 51 | List uploaders; 52 | DateTime createdAt; 53 | final String? readme; 54 | final String? changelog; 55 | List versions; 56 | List authors; 57 | List? dependencies; 58 | List tags; 59 | 60 | WebapiDetailView( 61 | this.name, 62 | this.version, 63 | this.description, 64 | this.homepage, 65 | this.uploaders, 66 | this.createdAt, 67 | this.readme, 68 | this.changelog, 69 | this.versions, 70 | this.authors, 71 | this.dependencies, 72 | this.tags); 73 | 74 | factory WebapiDetailView.fromJson(Map map) => 75 | _$WebapiDetailViewFromJson(map); 76 | 77 | Map toJson() => _$WebapiDetailViewToJson(this); 78 | } 79 | -------------------------------------------------------------------------------- /.idea/unpub-2.0.0.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /lib/src/models.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'models.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | UnpubVersion _$UnpubVersionFromJson(Map json) => UnpubVersion( 10 | json['version'] as String, 11 | json['pubspec'] as Map, 12 | json['pubspecYaml'] as String, 13 | json['uploader'] as String?, 14 | json['readme'] as String?, 15 | json['changelog'] as String?, 16 | identity(json['createdAt'] as DateTime), 17 | ); 18 | 19 | Map _$UnpubVersionToJson(UnpubVersion instance) { 20 | final val = { 21 | 'version': instance.version, 22 | 'pubspec': instance.pubspec, 23 | 'pubspecYaml': instance.pubspecYaml, 24 | }; 25 | 26 | void writeNotNull(String key, dynamic value) { 27 | if (value != null) { 28 | val[key] = value; 29 | } 30 | } 31 | 32 | writeNotNull('uploader', instance.uploader); 33 | writeNotNull('readme', instance.readme); 34 | writeNotNull('changelog', instance.changelog); 35 | writeNotNull('createdAt', identity(instance.createdAt)); 36 | return val; 37 | } 38 | 39 | UnpubPackage _$UnpubPackageFromJson(Map json) => UnpubPackage( 40 | json['name'] as String, 41 | (json['versions'] as List) 42 | .map((e) => UnpubVersion.fromJson(e as Map)) 43 | .toList(), 44 | json['private'] as bool, 45 | (json['uploaders'] as List).map((e) => e as String).toList(), 46 | identity(json['createdAt'] as DateTime), 47 | identity(json['updatedAt'] as DateTime), 48 | json['download'] as int?, 49 | ); 50 | 51 | Map _$UnpubPackageToJson(UnpubPackage instance) => 52 | { 53 | 'name': instance.name, 54 | 'versions': instance.versions, 55 | 'private': instance.private, 56 | 'uploaders': instance.uploaders, 57 | 'createdAt': identity(instance.createdAt), 58 | 'updatedAt': identity(instance.updatedAt), 59 | 'download': instance.download, 60 | }; 61 | 62 | UnpubQueryResult _$UnpubQueryResultFromJson(Map json) => 63 | UnpubQueryResult( 64 | json['count'] as int, 65 | (json['packages'] as List) 66 | .map((e) => UnpubPackage.fromJson(e as Map)) 67 | .toList(), 68 | ); 69 | 70 | Map _$UnpubQueryResultToJson(UnpubQueryResult instance) => 71 | { 72 | 'count': instance.count, 73 | 'packages': instance.packages, 74 | }; 75 | -------------------------------------------------------------------------------- /lib/src/mongo_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart/mongo_dart.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:unpub/src/models.dart'; 4 | import 'meta_store.dart'; 5 | 6 | final packageCollection = 'packages'; 7 | final statsCollection = 'stats'; 8 | 9 | class MongoStore extends MetaStore { 10 | Db db; 11 | 12 | MongoStore(this.db); 13 | 14 | static SelectorBuilder _selectByName(String? name) => where.eq('name', name); 15 | 16 | Future _queryPackagesBySelector( 17 | SelectorBuilder selector) async { 18 | final count = await db.collection(packageCollection).count(selector); 19 | final packages = await db 20 | .collection(packageCollection) 21 | .find(selector) 22 | .map((item) => UnpubPackage.fromJson(item)) 23 | .toList(); 24 | return UnpubQueryResult(count, packages); 25 | } 26 | 27 | @override 28 | queryPackage(name) async { 29 | var json = 30 | await db.collection(packageCollection).findOne(_selectByName(name)); 31 | if (json == null) return null; 32 | return UnpubPackage.fromJson(json); 33 | } 34 | 35 | @override 36 | addVersion(name, version) async { 37 | await db.collection(packageCollection).update( 38 | _selectByName(name), 39 | modify 40 | .push('versions', version.toJson()) 41 | .addToSet('uploaders', version.uploader) 42 | .setOnInsert('createdAt', version.createdAt) 43 | .setOnInsert('private', true) 44 | .setOnInsert('download', 0) 45 | .set('updatedAt', version.createdAt), 46 | upsert: true); 47 | } 48 | 49 | @override 50 | addUploader(name, email) async { 51 | await db 52 | .collection(packageCollection) 53 | .update(_selectByName(name), modify.push('uploaders', email)); 54 | } 55 | 56 | @override 57 | removeUploader(name, email) async { 58 | await db 59 | .collection(packageCollection) 60 | .update(_selectByName(name), modify.pull('uploaders', email)); 61 | } 62 | 63 | @override 64 | increaseDownloads(name, version) { 65 | var today = DateFormat('yyyyMMdd').format(DateTime.now()); 66 | db 67 | .collection(packageCollection) 68 | .update(_selectByName(name), modify.inc('download', 1)); 69 | db 70 | .collection(statsCollection) 71 | .update(_selectByName(name), modify.inc('d$today', 1)); 72 | } 73 | 74 | @override 75 | Future queryPackages({ 76 | required size, 77 | required page, 78 | required sort, 79 | keyword, 80 | uploader, 81 | dependency, 82 | }) { 83 | var selector = 84 | where.sortBy(sort, descending: true).limit(size).skip(page * size); 85 | 86 | if (keyword != null) { 87 | selector = selector.match('name', '.*$keyword.*'); 88 | } 89 | if (uploader != null) { 90 | selector = selector.eq('uploaders', uploader); 91 | } 92 | if (dependency != null) { 93 | selector = selector.raw({ 94 | 'versions': { 95 | r'$elemMatch': { 96 | 'pubspec.dependencies.$dependency': {r'$exists': true} 97 | } 98 | } 99 | }); 100 | } 101 | 102 | return _queryPackagesBySelector(selector); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/unpub_api/lib/models.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'models.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ListApi _$ListApiFromJson(Map json) => ListApi( 10 | json['count'] as int, 11 | (json['packages'] as List) 12 | .map((e) => ListApiPackage.fromJson(e as Map)) 13 | .toList(), 14 | ); 15 | 16 | Map _$ListApiToJson(ListApi instance) => { 17 | 'count': instance.count, 18 | 'packages': instance.packages, 19 | }; 20 | 21 | ListApiPackage _$ListApiPackageFromJson(Map json) => 22 | ListApiPackage( 23 | json['name'] as String, 24 | json['description'] as String?, 25 | (json['tags'] as List).map((e) => e as String).toList(), 26 | json['latest'] as String, 27 | DateTime.parse(json['updatedAt'] as String), 28 | ); 29 | 30 | Map _$ListApiPackageToJson(ListApiPackage instance) => 31 | { 32 | 'name': instance.name, 33 | 'description': instance.description, 34 | 'tags': instance.tags, 35 | 'latest': instance.latest, 36 | 'updatedAt': instance.updatedAt.toIso8601String(), 37 | }; 38 | 39 | DetailViewVersion _$DetailViewVersionFromJson(Map json) => 40 | DetailViewVersion( 41 | json['version'] as String, 42 | DateTime.parse(json['createdAt'] as String), 43 | ); 44 | 45 | Map _$DetailViewVersionToJson(DetailViewVersion instance) => 46 | { 47 | 'version': instance.version, 48 | 'createdAt': instance.createdAt.toIso8601String(), 49 | }; 50 | 51 | WebapiDetailView _$WebapiDetailViewFromJson(Map json) => 52 | WebapiDetailView( 53 | json['name'] as String, 54 | json['version'] as String, 55 | json['description'] as String, 56 | json['homepage'] as String, 57 | (json['uploaders'] as List).map((e) => e as String).toList(), 58 | DateTime.parse(json['createdAt'] as String), 59 | json['readme'] as String?, 60 | json['changelog'] as String?, 61 | (json['versions'] as List) 62 | .map((e) => DetailViewVersion.fromJson(e as Map)) 63 | .toList(), 64 | (json['authors'] as List).map((e) => e as String?).toList(), 65 | (json['dependencies'] as List?) 66 | ?.map((e) => e as String) 67 | .toList(), 68 | (json['tags'] as List).map((e) => e as String).toList(), 69 | ); 70 | 71 | Map _$WebapiDetailViewToJson(WebapiDetailView instance) => 72 | { 73 | 'name': instance.name, 74 | 'version': instance.version, 75 | 'description': instance.description, 76 | 'homepage': instance.homepage, 77 | 'uploaders': instance.uploaders, 78 | 'createdAt': instance.createdAt.toIso8601String(), 79 | 'readme': instance.readme, 80 | 'changelog': instance.changelog, 81 | 'versions': instance.versions, 82 | 'authors': instance.authors, 83 | 'dependencies': instance.dependencies, 84 | 'tags': instance.tags, 85 | }; 86 | -------------------------------------------------------------------------------- /test/unpub_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'package:collection/collection.dart'; 4 | import 'package:unpub/src/utils.dart'; 5 | import 'package:test/test.dart'; 6 | import 'package:path/path.dart' as path; 7 | import 'package:http/http.dart' as http; 8 | import 'package:mongo_dart/mongo_dart.dart'; 9 | import 'utils.dart'; 10 | import 'package:unpub/unpub.dart'; 11 | 12 | main() { 13 | Db _db = Db('mongodb://localhost:27017/dart_pub_test'); 14 | late HttpServer _server; 15 | 16 | setUpAll(() async { 17 | await _db.open(); 18 | }); 19 | 20 | Future> _readMeta(String name) async { 21 | var res = 22 | await _db.collection(packageCollection).findOne(where.eq('name', name)); 23 | res!.remove('_id'); // TODO: null 24 | return res; 25 | } 26 | 27 | Map _pubspecCache = {}; 28 | 29 | Future _readFile( 30 | String package, String version, String filename) async { 31 | var key = package + version + filename; 32 | if (_pubspecCache[key] == null) { 33 | var filePath = path.absolute('test/fixtures', package, version, filename); 34 | _pubspecCache[key] = await File(filePath).readAsString(); 35 | } 36 | return _pubspecCache[key]; 37 | } 38 | 39 | _cleanUpDb() async { 40 | await _db.dropCollection(packageCollection); 41 | await _db.dropCollection(statsCollection); 42 | } 43 | 44 | tearDownAll(() async { 45 | await _db.close(); 46 | }); 47 | 48 | group('publish', () { 49 | setUpAll(() async { 50 | await _cleanUpDb(); 51 | _server = await createServer(email0); 52 | }); 53 | 54 | tearDownAll(() async { 55 | await _server.close(); 56 | }); 57 | 58 | test('fresh', () async { 59 | var version = '0.0.1'; 60 | 61 | var result = await pubPublish(package0, version); 62 | expect(result.stderr, ''); 63 | 64 | var meta = await _readMeta(package0); 65 | 66 | expect(meta['name'], package0); 67 | expect(meta['uploaders'], [email0]); 68 | expect(meta['private'], true); 69 | expect(meta['createdAt'], isA()); 70 | expect(meta['updatedAt'], isA()); 71 | expect(meta['versions'], isList); 72 | expect(meta['versions'], hasLength(1)); 73 | 74 | var item = meta['versions'][0]; 75 | expect(item['createdAt'], isA()); 76 | item.remove('createdAt'); 77 | expect( 78 | DeepCollectionEquality().equals(item, { 79 | 'version': version, 80 | 'pubspecYaml': await _readFile(package0, version, 'pubspec.yaml'), 81 | 'pubspec': 82 | loadYamlAsMap(await _readFile(package0, version, 'pubspec.yaml')), 83 | 'readme': await _readFile(package0, version, 'README.md'), 84 | 'changelog': await _readFile(package0, version, 'CHANGELOG.md'), 85 | 'uploader': email0, 86 | }), 87 | true, 88 | ); 89 | }); 90 | 91 | test('existing package', () async { 92 | var version = '0.0.3'; 93 | 94 | var result = await pubPublish(package0, version); 95 | expect(result.stderr, ''); 96 | 97 | var meta = await _readMeta(package0); 98 | 99 | expect(meta['name'], package0); 100 | expect(meta['uploaders'], [email0]); 101 | expect(meta['versions'], isList); 102 | expect(meta['versions'], hasLength(2)); 103 | expect(meta['versions'][0]['version'], '0.0.1'); 104 | expect(meta['versions'][1]['version'], version); 105 | }); 106 | 107 | test('duplicated version', () async { 108 | var result = await pubPublish(package0, '0.0.3'); 109 | expect(result.stderr, contains('version invalid')); 110 | }); 111 | 112 | test('no readme and changelog', () async { 113 | var version = '1.0.0-noreadme'; 114 | var result = await pubPublish(package0, version); 115 | // expect(result.stderr, ''); // Suggestions: 116 | 117 | var meta = await _readMeta(package0); 118 | 119 | expect(meta['name'], package0); 120 | expect(meta['uploaders'], [email0]); 121 | expect(meta['versions'], isList); 122 | expect(meta['versions'], hasLength(3)); 123 | expect(meta['versions'][0]['version'], '0.0.1'); 124 | expect(meta['versions'][1]['version'], '0.0.3'); 125 | 126 | var item = meta['versions'][2]; 127 | expect(item['createdAt'], isA()); 128 | item.remove('createdAt'); 129 | expect( 130 | DeepCollectionEquality().equals(item, { 131 | 'version': version, 132 | 'pubspecYaml': await _readFile(package0, version, 'pubspec.yaml'), 133 | 'pubspec': 134 | loadYamlAsMap(await _readFile(package0, version, 'pubspec.yaml')), 135 | 'uploader': email0, 136 | }), 137 | true, 138 | ); 139 | }); 140 | }); 141 | 142 | group('get versions', () { 143 | setUpAll(() async { 144 | await _cleanUpDb(); 145 | _server = await createServer(email0); 146 | await pubPublish(package0, '0.0.1'); 147 | await pubPublish(package0, '0.0.2'); 148 | }); 149 | 150 | tearDownAll(() async { 151 | await _server.close(); 152 | }); 153 | 154 | test('existing at local', () async { 155 | var res = await getVersions(package0); 156 | expect(res.statusCode, HttpStatus.ok); 157 | 158 | var body = json.decode(res.body); 159 | expect( 160 | DeepCollectionEquality().equals(body, { 161 | "name": "package_0", 162 | "latest": { 163 | "archive_url": 164 | "$pubHostedUrl/packages/package_0/versions/0.0.2.tar.gz", 165 | "pubspec": loadYamlAsMap( 166 | await _readFile('package_0', '0.0.2', 'pubspec.yaml')), 167 | "version": "0.0.2" 168 | }, 169 | "versions": [ 170 | { 171 | "archive_url": 172 | "$pubHostedUrl/packages/package_0/versions/0.0.1.tar.gz", 173 | "pubspec": loadYamlAsMap( 174 | await _readFile('package_0', '0.0.1', 'pubspec.yaml')), 175 | "version": "0.0.1" 176 | }, 177 | { 178 | "archive_url": 179 | "$pubHostedUrl/packages/package_0/versions/0.0.2.tar.gz", 180 | "pubspec": loadYamlAsMap( 181 | await _readFile('package_0', '0.0.2', 'pubspec.yaml')), 182 | "version": "0.0.2" 183 | } 184 | ] 185 | }), 186 | true, 187 | ); 188 | }); 189 | 190 | test('existing at remote', () async { 191 | var name = 'http'; 192 | var res = await getVersions(name); 193 | expect(res.statusCode, HttpStatus.ok); 194 | 195 | var body = json.decode(res.body); 196 | expect(body['name'], name); 197 | }); 198 | 199 | test('not existing', () async { 200 | var res = await getVersions(notExistingPacakge); 201 | expect(res.statusCode, HttpStatus.notFound); 202 | }); 203 | }); 204 | 205 | group('get specific version', () { 206 | setUpAll(() async { 207 | await _cleanUpDb(); 208 | _server = await createServer(email0); 209 | await pubPublish(package0, '0.0.1'); 210 | await pubPublish(package0, '0.0.3+1'); 211 | }); 212 | 213 | tearDownAll(() async { 214 | await _server.close(); 215 | }); 216 | 217 | test('existing at local', () async { 218 | var res = await getSpecificVersion(package0, '0.0.1'); 219 | expect(res.statusCode, HttpStatus.ok); 220 | 221 | var body = json.decode(res.body); 222 | expect( 223 | DeepCollectionEquality().equals(body, { 224 | "archive_url": 225 | "$pubHostedUrl/packages/package_0/versions/0.0.1.tar.gz", 226 | "pubspec": loadYamlAsMap( 227 | await _readFile('package_0', '0.0.1', 'pubspec.yaml')), 228 | "version": '0.0.1' 229 | }), 230 | true, 231 | ); 232 | }); 233 | 234 | test('decode version correctly', () async { 235 | var res = await getSpecificVersion(package0, '0.0.3+1'); 236 | expect(res.statusCode, HttpStatus.ok); 237 | 238 | var body = json.decode(res.body); 239 | expect( 240 | DeepCollectionEquality().equals(body, { 241 | "archive_url": 242 | "$pubHostedUrl/packages/package_0/versions/0.0.3+1.tar.gz", 243 | "pubspec": loadYamlAsMap( 244 | await _readFile('package_0', '0.0.3+1', 'pubspec.yaml')), 245 | "version": '0.0.3+1' 246 | }), 247 | true, 248 | ); 249 | }); 250 | 251 | test('not existing version at local', () async { 252 | var res = await getSpecificVersion(package0, '0.0.2'); 253 | expect(res.statusCode, HttpStatus.notFound); 254 | }); 255 | 256 | test('existing at remote', () async { 257 | var res = await getSpecificVersion('http', '0.12.0+2'); 258 | expect(res.statusCode, HttpStatus.ok); 259 | 260 | var body = json.decode(res.body); 261 | expect(body['version'], '0.12.0+2'); 262 | }); 263 | 264 | test('not existing', () async { 265 | var res = await getSpecificVersion(notExistingPacakge, '0.0.1'); 266 | expect(res.statusCode, HttpStatus.notFound); 267 | }); 268 | }); 269 | 270 | group('uploader', () { 271 | setUpAll(() async { 272 | await _cleanUpDb(); 273 | _server = await createServer(email0); 274 | await pubPublish(package0, '0.0.1'); 275 | }); 276 | 277 | tearDownAll(() async { 278 | await _server.close(); 279 | }); 280 | 281 | group('add', () { 282 | test('already exists', () async { 283 | var result = await pubUploader(package0, 'add', email0); 284 | expect(result.stderr, contains('email already exists')); 285 | 286 | var meta = await _readMeta(package0); 287 | expect(meta['uploaders'], unorderedEquals([email0])); 288 | }); 289 | 290 | test('success', () async { 291 | var result = await pubUploader(package0, 'add', email1); 292 | expect(result.stderr, ''); 293 | 294 | var meta = await _readMeta(package0); 295 | expect(meta['uploaders'], unorderedEquals([email0, email1])); 296 | 297 | result = await pubUploader(package0, 'add', email2); 298 | expect(result.stderr, ''); 299 | 300 | meta = await _readMeta(package0); 301 | expect(meta['uploaders'], unorderedEquals([email0, email1, email2])); 302 | }); 303 | }); 304 | 305 | group('remove', () { 306 | test('not in uploader', () async { 307 | var result = await pubUploader(package0, 'remove', email3); 308 | expect(result.stderr, contains('email not uploader')); 309 | 310 | var meta = await _readMeta(package0); 311 | expect(meta['uploaders'], unorderedEquals([email0, email1, email2])); 312 | }); 313 | 314 | test('success', () async { 315 | var result = await pubUploader(package0, 'remove', email2); 316 | expect(result.stderr, ''); 317 | 318 | var meta = await _readMeta(package0); 319 | expect(meta['uploaders'], unorderedEquals([email0, email1])); 320 | 321 | result = await pubUploader(package0, 'remove', email1); 322 | expect(result.stderr, ''); 323 | 324 | meta = await _readMeta(package0); 325 | expect(meta['uploaders'], unorderedEquals([email0])); 326 | }); 327 | }); 328 | 329 | group('permission', () { 330 | setUpAll(() async { 331 | await _server.close(); 332 | _server = await createServer(email1); 333 | }); 334 | 335 | tearDownAll(() async { 336 | await _server.close(); 337 | }); 338 | 339 | test('add', () async { 340 | var result = await pubUploader(package0, 'add', email0); 341 | expect(result.stderr, contains('no permission')); 342 | }); 343 | 344 | test('remove', () async { 345 | var result = await pubUploader(package0, 'remove', email0); 346 | expect(result.stderr, contains('no permission')); 347 | }); 348 | }); 349 | }); 350 | 351 | group('badge', () { 352 | setUpAll(() async { 353 | await _cleanUpDb(); 354 | _server = await createServer(email0); 355 | await pubPublish(package0, '0.0.1'); 356 | }); 357 | 358 | tearDownAll(() async { 359 | await _server.close(); 360 | }); 361 | 362 | group('v', () { 363 | test('<1.0.0', () async { 364 | var res = await http.Client().send( 365 | http.Request('GET', baseUri.resolve('/badge/v/$package0')) 366 | ..followRedirects = false); 367 | expect(res.statusCode, HttpStatus.found); 368 | expect(res.headers[HttpHeaders.locationHeader], 369 | 'https://img.shields.io/static/v1?label=unpub&message=0.0.1&color=orange'); 370 | }); 371 | 372 | test('>=1.0.0', () async { 373 | await pubPublish(package0, '1.0.0'); 374 | 375 | var res = await http.Client().send( 376 | http.Request('GET', baseUri.resolve('/badge/v/$package0')) 377 | ..followRedirects = false); 378 | expect(res.statusCode, HttpStatus.found); 379 | expect(res.headers[HttpHeaders.locationHeader], 380 | 'https://img.shields.io/static/v1?label=unpub&message=1.0.0&color=blue'); 381 | }); 382 | 383 | test('package not exists', () async { 384 | var res = 385 | await http.get(baseUri.resolve('/badge/v/$notExistingPacakge')); 386 | expect(res.statusCode, HttpStatus.notFound); 387 | }); 388 | }); 389 | 390 | group('d', () { 391 | test('correct download count', () async { 392 | var res = await http.Client().send( 393 | http.Request('GET', baseUri.resolve('/badge/d/$package0')) 394 | ..followRedirects = false); 395 | expect(res.statusCode, HttpStatus.found); 396 | expect(res.headers[HttpHeaders.locationHeader], 397 | 'https://img.shields.io/static/v1?label=downloads&message=0&color=blue'); 398 | }); 399 | 400 | test('package not exists', () async { 401 | var res = 402 | await http.get(baseUri.resolve('/badge/d/$notExistingPacakge')); 403 | expect(res.statusCode, HttpStatus.notFound); 404 | }); 405 | }); 406 | }); 407 | } 408 | -------------------------------------------------------------------------------- /lib/src/app.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:collection/collection.dart' show IterableExtension; 4 | import 'package:shelf/shelf.dart' as shelf; 5 | import 'package:shelf/shelf_io.dart' as shelf_io; 6 | import 'package:http/http.dart' as http; 7 | import 'package:http/io_client.dart'; 8 | import 'package:googleapis/oauth2/v2.dart'; 9 | import 'package:mime/mime.dart'; 10 | import 'package:http_parser/http_parser.dart'; 11 | import 'package:shelf_cors_headers/shelf_cors_headers.dart'; 12 | import 'package:shelf_router/shelf_router.dart'; 13 | import 'package:pub_semver/pub_semver.dart' as semver; 14 | import 'package:archive/archive.dart'; 15 | import 'package:unpub/src/models.dart'; 16 | import 'package:unpub/unpub_api/lib/models.dart'; 17 | import 'package:unpub/src/meta_store.dart'; 18 | import 'package:unpub/src/package_store.dart'; 19 | import 'utils.dart'; 20 | import 'static/index.html.dart' as index_html; 21 | import 'static/main.dart.js.dart' as main_dart_js; 22 | 23 | part 'app.g.dart'; 24 | 25 | class App { 26 | /// meta information store 27 | final MetaStore metaStore; 28 | 29 | /// package(tarball) store 30 | final PackageStore packageStore; 31 | 32 | /// upstream url, default: https://pub.dev 33 | final String upstream; 34 | 35 | /// http(s) proxy to call googleapis (to get uploader email) 36 | final String? googleapisProxy; 37 | final String? overrideUploaderEmail; 38 | 39 | /// validate if the package can be published 40 | /// 41 | /// for more details, see: https://github.com/bytedance/unpub#package-validator 42 | final Future Function( 43 | Map pubspec, String uploaderEmail)? uploadValidator; 44 | 45 | App({ 46 | required this.metaStore, 47 | required this.packageStore, 48 | this.upstream = 'https://pub.dev', 49 | this.googleapisProxy, 50 | this.overrideUploaderEmail, 51 | this.uploadValidator, 52 | }); 53 | 54 | static shelf.Response _okWithJson(Map data) => 55 | shelf.Response.ok( 56 | json.encode(data), 57 | headers: { 58 | HttpHeaders.contentTypeHeader: ContentType.json.mimeType, 59 | 'Access-Control-Allow-Origin': '*' 60 | }, 61 | ); 62 | 63 | static shelf.Response _successMessage(String message) => _okWithJson({ 64 | 'success': {'message': message} 65 | }); 66 | 67 | static shelf.Response _badRequest(String message, 68 | {int status = HttpStatus.badRequest}) => 69 | shelf.Response( 70 | status, 71 | headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType}, 72 | body: json.encode({ 73 | 'error': {'message': message} 74 | }), 75 | ); 76 | 77 | http.Client? _googleapisClient; 78 | 79 | Future _getUploaderEmail(shelf.Request req) async { 80 | if (overrideUploaderEmail != null) return overrideUploaderEmail!; 81 | 82 | var authHeader = req.headers[HttpHeaders.authorizationHeader]; 83 | if (authHeader == null) throw 'missing authorization header'; 84 | 85 | var token = authHeader.split(' ').last; 86 | 87 | if (_googleapisClient == null) { 88 | if (googleapisProxy != null) { 89 | _googleapisClient = IOClient(HttpClient() 90 | ..findProxy = (url) => HttpClient.findProxyFromEnvironment(url, 91 | environment: {"https_proxy": googleapisProxy!})); 92 | } else { 93 | _googleapisClient = http.Client(); 94 | } 95 | } 96 | 97 | var info = 98 | await Oauth2Api(_googleapisClient!).tokeninfo(accessToken: token); 99 | if (info.email == null) throw 'fail to get google account email'; 100 | return info.email!; 101 | } 102 | 103 | Future serve([String? host = '0.0.0.0', int port = 4000]) async { 104 | var handler = const shelf.Pipeline() 105 | .addMiddleware(corsHeaders()) 106 | .addMiddleware(shelf.logRequests()) 107 | .addHandler((req) async { 108 | // Return 404 by default 109 | // https://github.com/google/dart-neats/issues/1 110 | var res = await router.call(req); 111 | return res; 112 | }); 113 | final hosts = host ?? ''; 114 | var server = await shelf_io.serve(handler, hosts, port); 115 | return server; 116 | } 117 | 118 | Map _versionToJson(UnpubVersion item, Uri baseUri) { 119 | var name = item.pubspec['name'] as String; 120 | var version = item.version; 121 | return { 122 | 'archive_url': baseUri 123 | .resolve('/packages/$name/versions/$version.tar.gz') 124 | .toString(), 125 | 'pubspec': item.pubspec, 126 | 'version': version, 127 | }; 128 | } 129 | 130 | bool isPubClient(shelf.Request req) { 131 | var ua = req.headers[HttpHeaders.userAgentHeader]; 132 | print(ua); 133 | return ua != null && ua.toLowerCase().contains('dart pub'); 134 | } 135 | 136 | Router get router => _$AppRouter(this); 137 | 138 | @Route.get('/api/packages/') 139 | Future getVersions(shelf.Request req, String name) async { 140 | var package = await metaStore.queryPackage(name); 141 | 142 | if (package == null) { 143 | return shelf.Response.found( 144 | Uri.parse(upstream).resolve('/api/packages/$name').toString()); 145 | } 146 | 147 | package.versions.sort((a, b) { 148 | return semver.Version.prioritize( 149 | semver.Version.parse(a.version), semver.Version.parse(b.version)); 150 | }); 151 | 152 | var versionMaps = package.versions 153 | .map((item) => _versionToJson(item, req.requestedUri)) 154 | .toList(); 155 | 156 | return _okWithJson({ 157 | 'name': name, 158 | 'latest': versionMaps.last, // TODO: Exclude pre release 159 | 'versions': versionMaps, 160 | }); 161 | } 162 | 163 | @Route.get('/api/packages//versions/') 164 | Future getVersion( 165 | shelf.Request req, String name, String version) async { 166 | // Important: + -> %2B, should be decoded here 167 | try { 168 | version = Uri.decodeComponent(version); 169 | } catch (err) { 170 | print(err); 171 | } 172 | 173 | var package = await metaStore.queryPackage(name); 174 | if (package == null) { 175 | return shelf.Response.found(Uri.parse(upstream) 176 | .resolve('/api/packages/$name/versions/$version') 177 | .toString()); 178 | } 179 | 180 | var packageVersion = 181 | package.versions.firstWhereOrNull((item) => item.version == version); 182 | if (packageVersion == null) { 183 | return shelf.Response.notFound('Not Found'); 184 | } 185 | 186 | return _okWithJson(_versionToJson(packageVersion, req.requestedUri)); 187 | } 188 | 189 | @Route.get('/packages//versions/.tar.gz') 190 | Future download( 191 | shelf.Request req, String name, String version) async { 192 | var package = await metaStore.queryPackage(name); 193 | if (package == null) { 194 | return shelf.Response.found(Uri.parse(upstream) 195 | .resolve('/packages/$name/versions/$version.tar.gz') 196 | .toString()); 197 | } 198 | 199 | if (isPubClient(req)) { 200 | metaStore.increaseDownloads(name, version); 201 | } 202 | 203 | if (packageStore.supportsDownloadUrl) { 204 | return shelf.Response.found(packageStore.downloadUrl(name, version)); 205 | } else { 206 | return shelf.Response.ok( 207 | packageStore.download(name, version), 208 | headers: {HttpHeaders.contentTypeHeader: ContentType.binary.mimeType}, 209 | ); 210 | } 211 | } 212 | 213 | @Route.get('/api/packages/versions/new') 214 | Future getUploadUrl(shelf.Request req) async { 215 | return _okWithJson({ 216 | 'url': req.requestedUri 217 | .resolve('/api/packages/versions/newUpload') 218 | .toString(), 219 | 'fields': {}, 220 | }); 221 | } 222 | 223 | @Route.post('/api/packages/versions/newUpload') 224 | Future upload(shelf.Request req) async { 225 | try { 226 | var uploader = '';//await _getUploaderEmail(req); 227 | 228 | var contentType = req.headers['content-type']; 229 | if (contentType == null) throw 'invalid content type'; 230 | 231 | var mediaType = MediaType.parse(contentType); 232 | var boundary = mediaType.parameters['boundary']; 233 | if (boundary == null) throw 'invalid boundary'; 234 | 235 | var transformer = MimeMultipartTransformer(boundary); 236 | MimeMultipart? fileData; 237 | 238 | // The map below makes the runtime type checker happy. 239 | // https://github.com/dart-lang/pub-dev/blob/19033f8154ca1f597ef5495acbc84a2bb368f16d/app/lib/fake/server/fake_storage_server.dart#L74 240 | final stream = req.read().map((a) => a).transform(transformer); 241 | await for (var part in stream) { 242 | if (fileData != null) continue; 243 | fileData = part; 244 | } 245 | 246 | var bb = await fileData!.fold( 247 | BytesBuilder(), (BytesBuilder byteBuilder, d) => byteBuilder..add(d)); 248 | var tarballBytes = bb.takeBytes(); 249 | var tarBytes = GZipDecoder().decodeBytes(tarballBytes); 250 | var archive = TarDecoder().decodeBytes(tarBytes); 251 | ArchiveFile? pubspecArchiveFile; 252 | ArchiveFile? readmeFile; 253 | ArchiveFile? changelogFile; 254 | 255 | for (var file in archive.files) { 256 | if (file.name == 'pubspec.yaml') { 257 | pubspecArchiveFile = file; 258 | continue; 259 | } 260 | if (file.name.toLowerCase() == 'readme.md') { 261 | readmeFile = file; 262 | continue; 263 | } 264 | if (file.name.toLowerCase() == 'changelog.md') { 265 | changelogFile = file; 266 | continue; 267 | } 268 | } 269 | 270 | if (pubspecArchiveFile == null) { 271 | throw 'Did not find any pubspec.yaml file in upload. Aborting.'; 272 | } 273 | 274 | var pubspecYaml = utf8.decode(pubspecArchiveFile.content); 275 | var pubspec = loadYamlAsMap(pubspecYaml)!; 276 | 277 | if (uploadValidator != null) { 278 | await uploadValidator!(pubspec, uploader); 279 | } 280 | 281 | // TODO: null 282 | var name = pubspec['name'] as String; 283 | var version = pubspec['version'] as String; 284 | 285 | var package = await metaStore.queryPackage(name); 286 | 287 | // Package already exists 288 | if (package != null) { 289 | if (package.private != true) { 290 | throw '$name is not a private package. Please upload it to https://pub.dev'; 291 | } 292 | 293 | // Check uploaders 294 | if (!package.uploaders.contains(uploader)) { 295 | throw '$uploader is not an uploader of $name'; 296 | } 297 | 298 | // Check duplicated version 299 | var duplicated = package.versions 300 | .firstWhereOrNull((item) => version == item.version); 301 | if (duplicated != null) { 302 | throw 'version invalid: $name@$version already exists.'; 303 | } 304 | } 305 | 306 | // Upload package tarball to storage 307 | await packageStore.upload(name, version, tarballBytes); 308 | 309 | String? readme; 310 | String? changelog; 311 | if (readmeFile != null) { 312 | readme = utf8.decode(readmeFile.content); 313 | } 314 | if (changelogFile != null) { 315 | changelog = utf8.decode(changelogFile.content); 316 | } 317 | 318 | // Write package meta to database 319 | var unpubVersion = UnpubVersion( 320 | version, 321 | pubspec, 322 | pubspecYaml, 323 | uploader, 324 | readme, 325 | changelog, 326 | DateTime.now(), 327 | ); 328 | await metaStore.addVersion(name, unpubVersion); 329 | 330 | // TODO: Upload docs 331 | return shelf.Response.found(req.requestedUri 332 | .resolve('/api/packages/versions/newUploadFinish') 333 | .toString()); 334 | } catch (err) { 335 | return shelf.Response.found(req.requestedUri 336 | .resolve('/api/packages/versions/newUploadFinish?error=$err')); 337 | } 338 | } 339 | 340 | @Route.get('/api/packages/versions/newUploadFinish') 341 | Future uploadFinish(shelf.Request req) async { 342 | var error = req.requestedUri.queryParameters['error']; 343 | if (error != null) { 344 | return _badRequest(error); 345 | } 346 | return _successMessage('Successfully uploaded package.'); 347 | } 348 | 349 | @Route.post('/api/packages//uploaders') 350 | Future addUploader(shelf.Request req, String name) async { 351 | var body = await req.readAsString(); 352 | var email = Uri.splitQueryString(body)['email']!; // TODO: null 353 | // var operatorEmail = await _getUploaderEmail(req); 354 | // var package = await metaStore.queryPackage(name); 355 | // 356 | // if (package?.uploaders.contains(operatorEmail) != true) { 357 | // return _badRequest('no permission', status: HttpStatus.forbidden); 358 | // } 359 | // if (package?.uploaders.contains(email) == true) { 360 | // return _badRequest('email already exists'); 361 | // } 362 | 363 | await metaStore.addUploader(name, email); 364 | return _successMessage('uploader added'); 365 | } 366 | 367 | @Route.delete('/api/packages//uploaders/') 368 | Future removeUploader( 369 | shelf.Request req, String name, String email) async { 370 | email = Uri.decodeComponent(email); 371 | // var operatorEmail = await _getUploaderEmail(req); 372 | // var package = await metaStore.queryPackage(name); 373 | // 374 | // // TODO: null 375 | // if (!package!.uploaders.contains(operatorEmail)) { 376 | // return _badRequest('no permission', status: HttpStatus.forbidden); 377 | // } 378 | // if (!package.uploaders.contains(email)) { 379 | // return _badRequest('email not uploader'); 380 | // } 381 | 382 | await metaStore.removeUploader(name, email); 383 | return _successMessage('uploader removed'); 384 | } 385 | 386 | @Route.get('/webapi/packages') 387 | Future getPackages(shelf.Request req) async { 388 | var params = req.requestedUri.queryParameters; 389 | var size = int.tryParse(params['size'] ?? '') ?? 10; 390 | var page = int.tryParse(params['page'] ?? '') ?? 0; 391 | var sort = params['sort'] ?? 'download'; 392 | var q = params['q']; 393 | 394 | String? keyword; 395 | String? uploader; 396 | String? dependency; 397 | 398 | if (q == null) { 399 | } else if (q.startsWith('email:')) { 400 | uploader = q.substring(6).trim(); 401 | } else if (q.startsWith('dependency:')) { 402 | dependency = q.substring(11).trim(); 403 | } else { 404 | keyword = q; 405 | } 406 | 407 | final result = await metaStore.queryPackages( 408 | size: size, 409 | page: page, 410 | sort: sort, 411 | keyword: keyword, 412 | uploader: uploader, 413 | dependency: dependency, 414 | ); 415 | 416 | var data = ListApi(result.count, [ 417 | for (var package in result.packages) 418 | ListApiPackage( 419 | package.name, 420 | package.versions.last.pubspec['description'] as String?, 421 | getPackageTags(package.versions.last.pubspec), 422 | package.versions.last.version, 423 | package.updatedAt, 424 | ) 425 | ]); 426 | 427 | return _okWithJson({'data': data.toJson()}); 428 | } 429 | 430 | @Route.get('/webapi/package//') 431 | Future getPackageDetail( 432 | shelf.Request req, String name, String version) async { 433 | var package = await metaStore.queryPackage(name); 434 | if (package == null) { 435 | return _okWithJson({'error': 'package not exists'}); 436 | } 437 | 438 | UnpubVersion? packageVersion; 439 | if (version == 'latest') { 440 | packageVersion = package.versions.last; 441 | } else { 442 | packageVersion = 443 | package.versions.firstWhereOrNull((item) => item.version == version); 444 | } 445 | if (packageVersion == null) { 446 | return _okWithJson({'error': 'version not exists'}); 447 | } 448 | 449 | var versions = package.versions 450 | .map((v) => DetailViewVersion(v.version, v.createdAt)) 451 | .toList(); 452 | versions.sort((a, b) { 453 | return semver.Version.prioritize( 454 | semver.Version.parse(b.version), semver.Version.parse(a.version)); 455 | }); 456 | 457 | var pubspec = packageVersion.pubspec; 458 | List authors; 459 | if (pubspec['author'] != null) { 460 | authors = RegExp(r'<(.*?)>') 461 | .allMatches(pubspec['author']) 462 | .map((match) => match.group(1)) 463 | .toList(); 464 | } else if (pubspec['authors'] != null) { 465 | authors = (pubspec['authors'] as List) 466 | .map((author) => RegExp(r'<(.*?)>').firstMatch(author)!.group(1)) 467 | .toList(); 468 | } else { 469 | authors = []; 470 | } 471 | 472 | var depMap = (pubspec['dependencies'] as Map? ?? {}).cast(); 473 | 474 | var data = WebapiDetailView( 475 | package.name, 476 | packageVersion.version, 477 | packageVersion.pubspec['description'] ?? '', 478 | packageVersion.pubspec['homepage'] ?? '', 479 | package.uploaders, 480 | packageVersion.createdAt, 481 | packageVersion.readme, 482 | packageVersion.changelog, 483 | versions, 484 | authors, 485 | depMap.keys.toList(), 486 | getPackageTags(packageVersion.pubspec), 487 | ); 488 | 489 | return _okWithJson({'data': data.toJson()}); 490 | } 491 | 492 | @Route.get('/') 493 | @Route.get('/packages') 494 | @Route.get('/packages/') 495 | @Route.get('/packages//versions/') 496 | Future indexHtml(shelf.Request req) async { 497 | return shelf.Response.ok(index_html.content, 498 | headers: {HttpHeaders.contentTypeHeader: ContentType.html.mimeType}); 499 | } 500 | 501 | @Route.get('/main.dart.js') 502 | Future mainDartJs(shelf.Request req) async { 503 | return shelf.Response.ok(main_dart_js.content, 504 | headers: {HttpHeaders.contentTypeHeader: 'text/javascript'}); 505 | } 506 | 507 | String _getBadgeUrl(String label, String message, String color, 508 | Map queryParameters) { 509 | var badgeUri = Uri.parse('https://img.shields.io/static/v1'); 510 | return Uri( 511 | scheme: badgeUri.scheme, 512 | host: badgeUri.host, 513 | path: badgeUri.path, 514 | queryParameters: { 515 | 'label': label, 516 | 'message': message, 517 | 'color': color, 518 | ...queryParameters, 519 | }).toString(); 520 | } 521 | 522 | @Route.get('/badge//') 523 | Future badge( 524 | shelf.Request req, String type, String name) async { 525 | var queryParameters = req.requestedUri.queryParameters; 526 | var package = await metaStore.queryPackage(name); 527 | if (package == null) { 528 | return shelf.Response.notFound('Not found'); 529 | } 530 | 531 | switch (type) { 532 | case 'v': 533 | var latest = semver.Version.primary(package.versions 534 | .map((pv) => semver.Version.parse(pv.version)) 535 | .toList()); 536 | 537 | var color = latest.major == 0 ? 'orange' : 'blue'; 538 | 539 | return shelf.Response.found( 540 | _getBadgeUrl('unpub', latest.toString(), color, queryParameters)); 541 | case 'd': 542 | return shelf.Response.found(_getBadgeUrl( 543 | 'downloads', package.download.toString(), 'blue', queryParameters)); 544 | default: 545 | return shelf.Response.notFound('Not found'); 546 | } 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /lib/src/static/index.html.dart: -------------------------------------------------------------------------------- 1 | const content = """ 2 | 3 | 4 | Unpub 5 | 6 | 7 | 8 | 12 | 13 | 1300 | 1700 | 1701 | 1702 | 1703 | 1704 | Loading... 1705 | 1706 | 1707 | """; 1708 | --------------------------------------------------------------------------------