├── .github └── workflows │ └── lint.yml ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── Makefile ├── README.md ├── assets └── screenshot.png ├── unpub ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin │ └── unpub.dart ├── example │ └── main.dart ├── lib │ ├── src │ │ ├── app.dart │ │ ├── app.g.dart │ │ ├── file_store.dart │ │ ├── meta_store.dart │ │ ├── models.dart │ │ ├── models.g.dart │ │ ├── mongo_store.dart │ │ ├── package_store.dart │ │ ├── static │ │ │ ├── index.html.dart │ │ │ └── main.dart.js.dart │ │ └── utils.dart │ ├── unpub.dart │ └── unpub_api │ │ ├── lib │ │ ├── models.dart │ │ └── models.g.dart │ │ └── pubspec.yaml ├── pubspec.yaml ├── test │ ├── file_store_test.dart │ ├── fixtures │ │ ├── file_store │ │ │ └── .gitignore │ │ ├── package_0 │ │ │ ├── 0.0.1 │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ └── pubspec.yaml │ │ │ ├── 0.0.2 │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ └── pubspec.yaml │ │ │ ├── 0.0.3+1 │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ └── pubspec.yaml │ │ │ ├── 0.0.3 │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ └── pubspec.yaml │ │ │ ├── 1.0.0-noreadme │ │ │ │ ├── LICENSE │ │ │ │ └── pubspec.yaml │ │ │ └── 1.0.0 │ │ │ │ ├── LICENSE │ │ │ │ └── pubspec.yaml │ │ └── package_1 │ │ │ └── 0.0.1 │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── pubspec.yaml │ ├── unpub_test.dart │ └── utils.dart └── tool │ └── pre_publish.dart ├── unpub_auth ├── .idea │ ├── .gitignore │ ├── libraries │ │ ├── Dart_Packages.xml │ │ └── Dart_SDK.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin │ └── unpub_auth.dart ├── lib │ ├── credentials_ext.dart │ ├── unpub_auth.dart │ └── utils.dart ├── pubspec.yaml └── unpub_auth.iml ├── unpub_aws ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── example │ └── main.dart ├── lib │ ├── core │ │ └── aws_credentials.dart │ ├── s3 │ │ └── s3_file_store.dart │ └── unpub_aws.dart ├── pubspec.yaml └── test │ └── s3_store_test.dart └── unpub_web ├── .gitignore ├── README.md ├── lib ├── app_component.css ├── app_component.dart ├── app_component.html ├── app_service.dart ├── constants.dart └── src │ ├── detail_component.dart │ ├── detail_component.html │ ├── home_component.dart │ ├── home_component.html │ ├── list_component.dart │ ├── list_component.html │ └── routes.dart ├── pubspec.yaml └── web ├── index.html └── main.dart /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | - push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | container: 8 | image: google/dart:latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: cd unpub && dart pub get && dart analyze 12 | - run: dart format --set-exit-if-changed **/*.dart 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | */.dart_tool/ 3 | */.packages 4 | 5 | # Conventional directory for build output. 6 | */build/ 7 | 8 | # pubspec.lock 9 | */pubspec.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "unpub", 9 | "cwd": "unpub", 10 | "request": "launch", 11 | "type": "dart", 12 | "args": ["--database", "mongodb://localhost:27017/dart_pub"], 13 | "program": "bin/unpub.dart", 14 | }, 15 | { 16 | "name": "unpub_aws", 17 | "cwd": "unpub_aws", 18 | "request": "launch", 19 | "type": "dart" 20 | }, 21 | { 22 | "name": "unpub_web", 23 | "cwd": "unpub_web", 24 | "request": "launch", 25 | "type": "dart" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dev-web: 2 | cd unpub_web &&\ 3 | dart pub global activate webdev 2.7.4 &&\ 4 | dart pub global activate webdev_proxy 0.1.1 &&\ 5 | dart pub global run webdev_proxy serve -- --auto=refresh --log-requests 6 | 7 | dev-api: 8 | cd unpub && dart run build_runner watch 9 | 10 | build: 11 | cd unpub_web &&\ 12 | dart pub global activate webdev 2.7.4 &&\ 13 | dart pub global run webdev build 14 | dart unpub/tool/pre_publish.dart 15 | dart format **/*.dart 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | unpub/README.md -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/assets/screenshot.png -------------------------------------------------------------------------------- /unpub/.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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/README.md: -------------------------------------------------------------------------------- 1 | # Unpub 2 | 3 | [![pub](https://img.shields.io/pub/v/unpub.svg)](https://pub.dev/packages/unpub) 4 | 5 | Unpub is a self-hosted private Dart Pub server for Enterprise, with a simple web interface to search and view packages information. 6 | 7 | ## Screenshots 8 | 9 | ![Screenshot](https://raw.githubusercontent.com/bytedance/unpub/master/assets/screenshot.png) 10 | 11 | ## Usage 12 | 13 | ### Command Line 14 | 15 | ```sh 16 | pub global activate unpub 17 | unpub --database mongodb://localhost:27017/dart_pub # Replace this with production database uri 18 | ``` 19 | 20 | Unpub use mongodb as meta information store and file system as package(tarball) store by default. 21 | 22 | Dart API is also available for further customization. 23 | 24 | ### Dart API 25 | 26 | ```dart 27 | import 'package:mongo_dart/mongo_dart.dart'; 28 | import 'package:unpub/unpub.dart' as unpub; 29 | 30 | main(List args) async { 31 | final db = Db('mongodb://localhost:27017/dart_pub'); 32 | await db.open(); // make sure the MongoDB connection opened 33 | 34 | final app = unpub.App( 35 | metaStore: unpub.MongoStore(db), 36 | packageStore: unpub.FileStore('./unpub-packages'), 37 | ); 38 | 39 | final server = await app.serve('0.0.0.0', 4000); 40 | print('Serving at http://${server.address.host}:${server.port}'); 41 | } 42 | ``` 43 | 44 | ### Options 45 | 46 | | Option | Description | Default | 47 | | --- | --- | --- | 48 | | `metaStore` (Required) | Meta information store | - | 49 | | `packageStore` (Required) | Package(tarball) store | - | 50 | | `upstream` | Upstream url | https://pub.dev | 51 | | `googleapisProxy` | Http(s) proxy to call googleapis (to get uploader email) | - | 52 | | `uploadValidator` | See [Package validator](#package-validator) | - | 53 | 54 | 55 | ### Usage behind reverse-proxy 56 | 57 | Using unpub behind reverse proxy(nginx or another), ensure you have necessary headers 58 | ```sh 59 | proxy_set_header X-Forwarded-Host $host; 60 | proxy_set_header X-Forwarded-Server $host; 61 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 62 | proxy_set_header X-Forwarded-Proto $scheme; 63 | 64 | # Workaround for: 65 | # Asynchronous error HttpException: 66 | # Trying to set 'Transfer-Encoding: Chunked' on HTTP 1.0 headers 67 | proxy_http_version 1.1; 68 | ``` 69 | 70 | ### Package validator 71 | 72 | Naming conflicts is a common issue for private registry. A reasonable solution is to add prefix to reduce conflict probability. 73 | 74 | With `uploadValidator` you could check if uploaded package is valid. 75 | 76 | ```dart 77 | var app = unpub.App( 78 | // ... 79 | uploadValidator: (Map pubspec, String uploaderEmail) { 80 | // Only allow packages with some specified prefixes to be uploaded 81 | var prefix = 'my_awesome_prefix_'; 82 | var name = pubspec['name'] as String; 83 | if (!name.startsWith(prefix)) { 84 | throw 'Package name should starts with $prefix'; 85 | } 86 | 87 | // Also, you can check if uploader email is valid 88 | if (!uploaderEmail.endsWith('@your-company.com')) { 89 | throw 'Uploader email invalid'; 90 | } 91 | } 92 | ); 93 | ``` 94 | 95 | ### Customize meta and package store 96 | 97 | Unpub is designed to be extensible. It is quite easy to customize your own meta store and package store. 98 | 99 | ```dart 100 | import 'package:unpub/unpub.dart' as unpub; 101 | 102 | class MyAwesomeMetaStore extends unpub.MetaStore { 103 | // Implement methods of MetaStore abstract class 104 | // ... 105 | } 106 | 107 | class MyAwesomePackageStore extends unpub.PackageStore { 108 | // Implement methods of PackageStore abstract class 109 | // ... 110 | } 111 | 112 | // Then use it 113 | var app = unpub.App( 114 | metaStore: MyAwesomeMetaStore(), 115 | packageStore: MyAwesomePackageStore(), 116 | ); 117 | ``` 118 | 119 | #### Available Package Stores 120 | 121 | 1. [unpub_aws](https://github.com/bytedance/unpub/tree/master/unpub_aws): AWS S3 package store, maintained by [@CleanCode](https://github.com/Clean-Cole). 122 | 123 | ## Badges 124 | 125 | | URL | Badge | 126 | | --- | --- | 127 | | `/badge/v/{package_name}` | ![badge example](https://img.shields.io/static/v1?label=unpub&message=0.1.0&color=orange) ![badge example](https://img.shields.io/static/v1?label=unpub&message=1.0.0&color=blue) | 128 | | `/badge/d/{package_name}` | ![badge example](https://img.shields.io/static/v1?label=downloads&message=123&color=blue) | 129 | 130 | ## Alternatives 131 | 132 | - [pub-dev](https://github.com/dart-lang/pub-dev): Source code of [pub.dev](https://pub.dev), which should be deployed at Google Cloud Platform. 133 | - [pub_server](https://github.com/dart-lang/pub_server): An alpha version of pub server provided by Dart team. 134 | 135 | ## Credits 136 | 137 | - [pub-dev](https://github.com/dart-lang/pub-dev): Web page styles are mostly imported from https://pub.dev directly. 138 | - [shields](https://shields.io): Badges generation. 139 | 140 | ## License 141 | 142 | MIT 143 | -------------------------------------------------------------------------------- /unpub/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', 12 | abbr: 'd', defaultsTo: 'mongodb://localhost:27017/dart_pub'); 13 | parser.addOption('proxy-origin', abbr: 'o', defaultsTo: ''); 14 | 15 | var results = parser.parse(args); 16 | 17 | var host = results['host'] as String; 18 | var port = int.parse(results['port'] as String); 19 | var dbUri = results['database'] as String; 20 | var proxy_origin = results['proxy-origin'] as String; 21 | 22 | if (results.rest.isNotEmpty) { 23 | print('Got unexpected arguments: "${results.rest.join(' ')}".\n\nUsage:\n'); 24 | print(parser.usage); 25 | exit(1); 26 | } 27 | 28 | final db = Db(dbUri); 29 | await db.open(); 30 | 31 | var baseDir = path.absolute('unpub-packages'); 32 | 33 | var app = unpub.App( 34 | metaStore: unpub.MongoStore(db), 35 | packageStore: unpub.FileStore(baseDir), 36 | proxy_origin: proxy_origin.trim().isEmpty ? null : Uri.parse(proxy_origin) 37 | ); 38 | 39 | var server = await app.serve(host, port); 40 | print('Serving at http://${server.address.host}:${server.port}'); 41 | } 42 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | static const proxyOriginHeader = "proxy-origin"; 27 | 28 | /// meta information store 29 | final MetaStore metaStore; 30 | 31 | /// package(tarball) store 32 | final PackageStore packageStore; 33 | 34 | /// upstream url, default: https://pub.dev 35 | final String upstream; 36 | 37 | /// http(s) proxy to call googleapis (to get uploader email) 38 | final String? googleapisProxy; 39 | final String? overrideUploaderEmail; 40 | 41 | /// A forward proxy uri 42 | final Uri? proxy_origin; 43 | 44 | /// validate if the package can be published 45 | /// 46 | /// for more details, see: https://github.com/bytedance/unpub#package-validator 47 | final Future Function( 48 | Map pubspec, String uploaderEmail)? uploadValidator; 49 | 50 | App({ 51 | required this.metaStore, 52 | required this.packageStore, 53 | this.upstream = 'https://pub.dev', 54 | this.googleapisProxy, 55 | this.overrideUploaderEmail, 56 | this.uploadValidator, 57 | this.proxy_origin, 58 | }); 59 | 60 | static shelf.Response _okWithJson(Map data) => 61 | shelf.Response.ok( 62 | json.encode(data), 63 | headers: { 64 | HttpHeaders.contentTypeHeader: ContentType.json.mimeType, 65 | 'Access-Control-Allow-Origin': '*' 66 | }, 67 | ); 68 | 69 | static shelf.Response _successMessage(String message) => _okWithJson({ 70 | 'success': {'message': message} 71 | }); 72 | 73 | static shelf.Response _badRequest(String message, 74 | {int status = HttpStatus.badRequest}) => 75 | shelf.Response( 76 | status, 77 | headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType}, 78 | body: json.encode({ 79 | 'error': {'message': message} 80 | }), 81 | ); 82 | 83 | http.Client? _googleapisClient; 84 | 85 | String _resolveUrl(shelf.Request req, String reference) { 86 | if (proxy_origin != null) { 87 | return proxy_origin!.resolve(reference).toString(); 88 | } 89 | String? proxyOriginInHeader = req.headers[proxyOriginHeader]; 90 | if (proxyOriginInHeader != null) { 91 | return Uri.parse(proxyOriginInHeader).resolve(reference).toString(); 92 | } 93 | return req.requestedUri.resolve(reference).toString(); 94 | } 95 | 96 | Future _getUploaderEmail(shelf.Request req) async { 97 | if (overrideUploaderEmail != null) return overrideUploaderEmail!; 98 | 99 | var authHeader = req.headers[HttpHeaders.authorizationHeader]; 100 | if (authHeader == null) throw 'missing authorization header'; 101 | 102 | var token = authHeader.split(' ').last; 103 | 104 | if (_googleapisClient == null) { 105 | if (googleapisProxy != null) { 106 | _googleapisClient = IOClient(HttpClient() 107 | ..findProxy = (url) => HttpClient.findProxyFromEnvironment(url, 108 | environment: {"https_proxy": googleapisProxy!})); 109 | } else { 110 | _googleapisClient = http.Client(); 111 | } 112 | } 113 | 114 | var info = 115 | await Oauth2Api(_googleapisClient!).tokeninfo(accessToken: token); 116 | if (info.email == null) throw 'fail to get google account email'; 117 | return info.email!; 118 | } 119 | 120 | Future serve([String host = '0.0.0.0', int port = 4000]) async { 121 | var handler = const shelf.Pipeline() 122 | .addMiddleware(corsHeaders()) 123 | .addMiddleware(shelf.logRequests()) 124 | .addHandler((req) async { 125 | // Return 404 by default 126 | // https://github.com/google/dart-neats/issues/1 127 | var res = await router.call(req); 128 | return res; 129 | }); 130 | var server = await shelf_io.serve(handler, host, port); 131 | return server; 132 | } 133 | 134 | Map _versionToJson(UnpubVersion item, shelf.Request req) { 135 | var name = item.pubspec['name'] as String; 136 | var version = item.version; 137 | return { 138 | 'archive_url': _resolveUrl(req, '/packages/$name/versions/$version.tar.gz'), 139 | 'pubspec': item.pubspec, 140 | 'version': version, 141 | }; 142 | } 143 | 144 | bool isPubClient(shelf.Request req) { 145 | var ua = req.headers[HttpHeaders.userAgentHeader]; 146 | print(ua); 147 | return ua != null && ua.toLowerCase().contains('dart pub'); 148 | } 149 | 150 | Router get router => _$AppRouter(this); 151 | 152 | @Route.get('/api/packages/') 153 | Future getVersions(shelf.Request req, String name) async { 154 | var package = await metaStore.queryPackage(name); 155 | 156 | if (package == null) { 157 | return shelf.Response.found( 158 | Uri.parse(upstream).resolve('/api/packages/$name').toString()); 159 | } 160 | 161 | package.versions.sort((a, b) { 162 | return semver.Version.prioritize( 163 | semver.Version.parse(a.version), semver.Version.parse(b.version)); 164 | }); 165 | 166 | var versionMaps = package.versions 167 | .map((item) => _versionToJson(item, req)) 168 | .toList(); 169 | 170 | return _okWithJson({ 171 | 'name': name, 172 | 'latest': versionMaps.last, // TODO: Exclude pre release 173 | 'versions': versionMaps, 174 | }); 175 | } 176 | 177 | @Route.get('/api/packages//versions/') 178 | Future getVersion( 179 | shelf.Request req, String name, String version) async { 180 | // Important: + -> %2B, should be decoded here 181 | try { 182 | version = Uri.decodeComponent(version); 183 | } catch (err) { 184 | print(err); 185 | } 186 | 187 | var package = await metaStore.queryPackage(name); 188 | if (package == null) { 189 | return shelf.Response.found(Uri.parse(upstream) 190 | .resolve('/api/packages/$name/versions/$version') 191 | .toString()); 192 | } 193 | 194 | var packageVersion = 195 | package.versions.firstWhereOrNull((item) => item.version == version); 196 | if (packageVersion == null) { 197 | return shelf.Response.notFound('Not Found'); 198 | } 199 | 200 | return _okWithJson(_versionToJson(packageVersion, req)); 201 | } 202 | 203 | @Route.get('/packages//versions/.tar.gz') 204 | Future download( 205 | shelf.Request req, String name, String version) async { 206 | var package = await metaStore.queryPackage(name); 207 | if (package == null) { 208 | return shelf.Response.found(Uri.parse(upstream) 209 | .resolve('/packages/$name/versions/$version.tar.gz') 210 | .toString()); 211 | } 212 | 213 | if (isPubClient(req)) { 214 | metaStore.increaseDownloads(name, version); 215 | } 216 | 217 | if (packageStore.supportsDownloadUrl) { 218 | return shelf.Response.found( 219 | await packageStore.downloadUrl(name, version)); 220 | } else { 221 | return shelf.Response.ok( 222 | packageStore.download(name, version), 223 | headers: {HttpHeaders.contentTypeHeader: ContentType.binary.mimeType}, 224 | ); 225 | } 226 | } 227 | 228 | @Route.get('/api/packages/versions/new') 229 | Future getUploadUrl(shelf.Request req) async { 230 | return _okWithJson({ 231 | 'url': _resolveUrl(req, '/api/packages/versions/newUpload') 232 | .toString(), 233 | 'fields': {}, 234 | }); 235 | } 236 | 237 | @Route.post('/api/packages/versions/newUpload') 238 | Future upload(shelf.Request req) async { 239 | try { 240 | var uploader = await _getUploaderEmail(req); 241 | 242 | var contentType = req.headers['content-type']; 243 | if (contentType == null) throw 'invalid content type'; 244 | 245 | var mediaType = MediaType.parse(contentType); 246 | var boundary = mediaType.parameters['boundary']; 247 | if (boundary == null) throw 'invalid boundary'; 248 | 249 | var transformer = MimeMultipartTransformer(boundary); 250 | MimeMultipart? fileData; 251 | 252 | // The map below makes the runtime type checker happy. 253 | // https://github.com/dart-lang/pub-dev/blob/19033f8154ca1f597ef5495acbc84a2bb368f16d/app/lib/fake/server/fake_storage_server.dart#L74 254 | final stream = req.read().map((a) => a).transform(transformer); 255 | await for (var part in stream) { 256 | if (fileData != null) continue; 257 | fileData = part; 258 | } 259 | 260 | var bb = await fileData!.fold( 261 | BytesBuilder(), (BytesBuilder byteBuilder, d) => byteBuilder..add(d)); 262 | var tarballBytes = bb.takeBytes(); 263 | var tarBytes = GZipDecoder().decodeBytes(tarballBytes); 264 | var archive = TarDecoder().decodeBytes(tarBytes); 265 | ArchiveFile? pubspecArchiveFile; 266 | ArchiveFile? readmeFile; 267 | ArchiveFile? changelogFile; 268 | 269 | for (var file in archive.files) { 270 | if (file.name == 'pubspec.yaml') { 271 | pubspecArchiveFile = file; 272 | continue; 273 | } 274 | if (file.name.toLowerCase() == 'readme.md') { 275 | readmeFile = file; 276 | continue; 277 | } 278 | if (file.name.toLowerCase() == 'changelog.md') { 279 | changelogFile = file; 280 | continue; 281 | } 282 | } 283 | 284 | if (pubspecArchiveFile == null) { 285 | throw 'Did not find any pubspec.yaml file in upload. Aborting.'; 286 | } 287 | 288 | var pubspecYaml = utf8.decode(pubspecArchiveFile.content); 289 | var pubspec = loadYamlAsMap(pubspecYaml)!; 290 | 291 | if (uploadValidator != null) { 292 | await uploadValidator!(pubspec, uploader); 293 | } 294 | 295 | // TODO: null 296 | var name = pubspec['name'] as String; 297 | var version = pubspec['version'] as String; 298 | 299 | var package = await metaStore.queryPackage(name); 300 | 301 | // Package already exists 302 | if (package != null) { 303 | if (package.private == false) { 304 | throw '$name is not a private package. Please upload it to https://pub.dev'; 305 | } 306 | 307 | // Check uploaders 308 | if (package.uploaders?.contains(uploader) == false) { 309 | throw '$uploader is not an uploader of $name'; 310 | } 311 | 312 | // Check duplicated version 313 | var duplicated = package.versions 314 | .firstWhereOrNull((item) => version == item.version); 315 | if (duplicated != null) { 316 | throw 'version invalid: $name@$version already exists.'; 317 | } 318 | } 319 | 320 | // Upload package tarball to storage 321 | await packageStore.upload(name, version, tarballBytes); 322 | 323 | String? readme; 324 | String? changelog; 325 | if (readmeFile != null) { 326 | readme = utf8.decode(readmeFile.content); 327 | } 328 | if (changelogFile != null) { 329 | changelog = utf8.decode(changelogFile.content); 330 | } 331 | 332 | // Write package meta to database 333 | var unpubVersion = UnpubVersion( 334 | version, 335 | pubspec, 336 | pubspecYaml, 337 | uploader, 338 | readme, 339 | changelog, 340 | DateTime.now(), 341 | ); 342 | await metaStore.addVersion(name, unpubVersion); 343 | 344 | // TODO: Upload docs 345 | return shelf.Response.found(_resolveUrl(req, '/api/packages/versions/newUploadFinish')); 346 | } catch (err) { 347 | return shelf.Response.found(_resolveUrl(req, '/api/packages/versions/newUploadFinish?error=$err')); 348 | } 349 | } 350 | 351 | @Route.get('/api/packages/versions/newUploadFinish') 352 | Future uploadFinish(shelf.Request req) async { 353 | var error = req.requestedUri.queryParameters['error']; 354 | if (error != null) { 355 | return _badRequest(error); 356 | } 357 | return _successMessage('Successfully uploaded package.'); 358 | } 359 | 360 | @Route.post('/api/packages//uploaders') 361 | Future addUploader(shelf.Request req, String name) async { 362 | var body = await req.readAsString(); 363 | var email = Uri.splitQueryString(body)['email']!; // TODO: null 364 | var operatorEmail = await _getUploaderEmail(req); 365 | var package = await metaStore.queryPackage(name); 366 | 367 | if (package?.uploaders?.contains(operatorEmail) == false) { 368 | return _badRequest('no permission', status: HttpStatus.forbidden); 369 | } 370 | if (package?.uploaders?.contains(email) == true) { 371 | return _badRequest('email already exists'); 372 | } 373 | 374 | await metaStore.addUploader(name, email); 375 | return _successMessage('uploader added'); 376 | } 377 | 378 | @Route.delete('/api/packages//uploaders/') 379 | Future removeUploader( 380 | shelf.Request req, String name, String email) async { 381 | email = Uri.decodeComponent(email); 382 | var operatorEmail = await _getUploaderEmail(req); 383 | var package = await metaStore.queryPackage(name); 384 | 385 | // TODO: null 386 | if (package?.uploaders?.contains(operatorEmail) == false) { 387 | return _badRequest('no permission', status: HttpStatus.forbidden); 388 | } 389 | if (package?.uploaders?.contains(email) == false) { 390 | return _badRequest('email not uploader'); 391 | } 392 | 393 | await metaStore.removeUploader(name, email); 394 | return _successMessage('uploader removed'); 395 | } 396 | 397 | @Route.get('/webapi/packages') 398 | Future getPackages(shelf.Request req) async { 399 | var params = req.requestedUri.queryParameters; 400 | var size = int.tryParse(params['size'] ?? '') ?? 10; 401 | var page = int.tryParse(params['page'] ?? '') ?? 0; 402 | var sort = params['sort'] ?? 'download'; 403 | var q = params['q']; 404 | 405 | String? keyword; 406 | String? uploader; 407 | String? dependency; 408 | 409 | if (q == null) { 410 | } else if (q.startsWith('email:')) { 411 | uploader = q.substring(6).trim(); 412 | } else if (q.startsWith('dependency:')) { 413 | dependency = q.substring(11).trim(); 414 | } else { 415 | keyword = q; 416 | } 417 | 418 | final result = await metaStore.queryPackages( 419 | size: size, 420 | page: page, 421 | sort: sort, 422 | keyword: keyword, 423 | uploader: uploader, 424 | dependency: dependency, 425 | ); 426 | 427 | var data = ListApi(result.count, [ 428 | for (var package in result.packages) 429 | ListApiPackage( 430 | package.name, 431 | package.versions.last.pubspec['description'] as String?, 432 | getPackageTags(package.versions.last.pubspec), 433 | package.versions.last.version, 434 | package.updatedAt, 435 | ) 436 | ]); 437 | 438 | return _okWithJson({'data': data.toJson()}); 439 | } 440 | 441 | @Route.get('/packages/.json') 442 | Future getPackageVersions( 443 | shelf.Request req, String name) async { 444 | var package = await metaStore.queryPackage(name); 445 | if (package == null) { 446 | return _badRequest('package not exists', status: HttpStatus.notFound); 447 | } 448 | 449 | var versions = package.versions.map((v) => v.version).toList(); 450 | versions.sort((a, b) { 451 | return semver.Version.prioritize( 452 | semver.Version.parse(b), semver.Version.parse(a)); 453 | }); 454 | 455 | return _okWithJson({ 456 | 'name': name, 457 | 'versions': versions, 458 | }); 459 | } 460 | 461 | @Route.get('/webapi/package//') 462 | Future getPackageDetail( 463 | shelf.Request req, String name, String version) async { 464 | var package = await metaStore.queryPackage(name); 465 | if (package == null) { 466 | return _okWithJson({'error': 'package not exists'}); 467 | } 468 | 469 | UnpubVersion? packageVersion; 470 | if (version == 'latest') { 471 | packageVersion = package.versions.last; 472 | } else { 473 | packageVersion = 474 | package.versions.firstWhereOrNull((item) => item.version == version); 475 | } 476 | if (packageVersion == null) { 477 | return _okWithJson({'error': 'version not exists'}); 478 | } 479 | 480 | var versions = package.versions 481 | .map((v) => DetailViewVersion(v.version, v.createdAt)) 482 | .toList(); 483 | versions.sort((a, b) { 484 | return semver.Version.prioritize( 485 | semver.Version.parse(b.version), semver.Version.parse(a.version)); 486 | }); 487 | 488 | var pubspec = packageVersion.pubspec; 489 | List authors; 490 | if (pubspec['author'] != null) { 491 | authors = RegExp(r'<(.*?)>') 492 | .allMatches(pubspec['author']) 493 | .map((match) => match.group(1)) 494 | .toList(); 495 | } else if (pubspec['authors'] != null) { 496 | authors = (pubspec['authors'] as List) 497 | .map((author) => RegExp(r'<(.*?)>').firstMatch(author)!.group(1)) 498 | .toList(); 499 | } else { 500 | authors = []; 501 | } 502 | 503 | var depMap = (pubspec['dependencies'] as Map? ?? {}).cast(); 504 | 505 | var data = WebapiDetailView( 506 | package.name, 507 | packageVersion.version, 508 | packageVersion.pubspec['description'] ?? '', 509 | packageVersion.pubspec['homepage'] ?? '', 510 | package.uploaders ?? [], 511 | packageVersion.createdAt, 512 | packageVersion.readme, 513 | packageVersion.changelog, 514 | versions, 515 | authors, 516 | depMap.keys.toList(), 517 | getPackageTags(packageVersion.pubspec), 518 | ); 519 | 520 | return _okWithJson({'data': data.toJson()}); 521 | } 522 | 523 | @Route.get('/') 524 | @Route.get('/packages') 525 | @Route.get('/packages/') 526 | @Route.get('/packages//versions/') 527 | Future indexHtml(shelf.Request req) async { 528 | return shelf.Response.ok(index_html.content, 529 | headers: {HttpHeaders.contentTypeHeader: ContentType.html.mimeType}); 530 | } 531 | 532 | @Route.get('/main.dart.js') 533 | Future mainDartJs(shelf.Request req) async { 534 | return shelf.Response.ok(main_dart_js.content, 535 | headers: {HttpHeaders.contentTypeHeader: 'text/javascript'}); 536 | } 537 | 538 | String _getBadgeUrl(String label, String message, String color, 539 | Map queryParameters) { 540 | var badgeUri = Uri.parse('https://img.shields.io/static/v1'); 541 | return Uri( 542 | scheme: badgeUri.scheme, 543 | host: badgeUri.host, 544 | path: badgeUri.path, 545 | queryParameters: { 546 | 'label': label, 547 | 'message': message, 548 | 'color': color, 549 | ...queryParameters, 550 | }).toString(); 551 | } 552 | 553 | @Route.get('/badge//') 554 | Future badge( 555 | shelf.Request req, String type, String name) async { 556 | var queryParameters = req.requestedUri.queryParameters; 557 | var package = await metaStore.queryPackage(name); 558 | if (package == null) { 559 | return shelf.Response.notFound('Not found'); 560 | } 561 | 562 | switch (type) { 563 | case 'v': 564 | var latest = semver.Version.primary(package.versions 565 | .map((pv) => semver.Version.parse(pv.version)) 566 | .toList()); 567 | 568 | var color = latest.major == 0 ? 'orange' : 'blue'; 569 | 570 | return shelf.Response.found( 571 | _getBadgeUrl('unpub', latest.toString(), color, queryParameters)); 572 | case 'd': 573 | return shelf.Response.found(_getBadgeUrl( 574 | 'downloads', package.download.toString(), 'blue', queryParameters)); 575 | default: 576 | return shelf.Response.notFound('Not found'); 577 | } 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /unpub/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('GET', r'/packages/.json', service.getPackageVersions); 25 | router.add( 26 | 'GET', r'/webapi/package//', service.getPackageDetail); 27 | router.add('GET', r'/', service.indexHtml); 28 | router.add('GET', r'/packages', service.indexHtml); 29 | router.add('GET', r'/packages/', service.indexHtml); 30 | router.add('GET', r'/packages//versions/', service.indexHtml); 31 | router.add('GET', r'/main.dart.js', service.mainDartJs); 32 | router.add('GET', r'/badge//', service.badge); 33 | return router; 34 | } 35 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | }; 24 | 25 | void writeNotNull(String key, dynamic value) { 26 | if (value != null) { 27 | val[key] = value; 28 | } 29 | } 30 | 31 | writeNotNull('pubspecYaml', instance.pubspecYaml); 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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/lib/src/package_store.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | abstract class PackageStore { 4 | bool supportsDownloadUrl = false; 5 | 6 | FutureOr downloadUrl(String name, String version) { 7 | throw 'downloadUri not implemented'; 8 | } 9 | 10 | Stream> download(String name, String version) { 11 | throw 'download not implemented'; 12 | } 13 | 14 | Future upload(String name, String version, List content); 15 | } 16 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/lib/unpub_api/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: unpub_api 2 | 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | -------------------------------------------------------------------------------- /unpub/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.1-dev.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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/test/fixtures/file_store/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.1/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.1 2 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.1/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_0/0.0.1/LICENSE -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.1/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.1 2 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.2/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.2 2 | 3 | # 0.0.1 4 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.2/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_0/0.0.2/LICENSE -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.2/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.2 2 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.3+1/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_0/0.0.3+1/LICENSE -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.3+1/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.3+1 2 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.3/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.3 2 | 3 | # 0.0.2 4 | 5 | # 0.0.1 6 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.3/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_0/0.0.3/LICENSE -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/0.0.3/README.md: -------------------------------------------------------------------------------- 1 | # package0 0.0.3 2 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/1.0.0-noreadme/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_0/1.0.0-noreadme/LICENSE -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_0/1.0.0/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_0/1.0.0/LICENSE -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/test/fixtures/package_1/0.0.1/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_1/0.0.1/LICENSE -------------------------------------------------------------------------------- /unpub/test/fixtures/package_1/0.0.1/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/unpub/de8d01455cc09967e972841dc104fd9a4b959acc/unpub/test/fixtures/package_1/0.0.1/README.md -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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 | -------------------------------------------------------------------------------- /unpub/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""";\n'; 12 | File(path.absolute('unpub/lib/src/static', '${file}.dart')).writeAsStringSync(content); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /unpub_auth/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /unpub_auth/.idea/libraries/Dart_Packages.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | -------------------------------------------------------------------------------- /unpub_auth/.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /unpub_auth/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /unpub_auth/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /unpub_auth/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /unpub_auth/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0+3 2 | 3 | - Fix the dir is not exist when never use it. 4 | 5 | ## 0.1.0+2 6 | 7 | - Fix documents format. 8 | 9 | ## 0.1.0+1 10 | 11 | - Fix documents format. 12 | 13 | ## 0.1.0 14 | 15 | - unpub_auth use independent auth flow. 16 | 17 | ## 0.0.1 18 | 19 | - Initial version. 20 | -------------------------------------------------------------------------------- /unpub_auth/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ByteDance Inc. 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 | -------------------------------------------------------------------------------- /unpub_auth/README.md: -------------------------------------------------------------------------------- 1 | # unpub_auth 2 | 3 | Only for Dart 2.15 and later. 4 | 5 | Since Dart 2.15: 6 | 7 | 1. The `accessToken` is only sent to https://pub.dev and https://pub.dartlang.org. See [dart-lang/pub #3007](https://github.com/dart-lang/pub/pull/3007) for details. 8 | 2. Since Dart 2.15, the third-party pub's token is stored at `/Users/username/Library/Application Support/dart/pub-tokens.json` (macOS) 9 | 10 | So the self-hosted pub server should have its own auth flow. unpub is using Google OAuth2 by default. 11 | 12 | - `unpub_auth login` will generate `unpub-credentials.json` locally after developer login the `unpub_auth`. 13 | - Before calling `dart pub publish` or `flutter pub publish`, please call `unpub_auth get | dart pub token add ` first. 14 | - `unpub_auth get` will refresh the token. New accessToken will be write to `pub-tokens.json` by `dart pub token add `. 15 | - So you can always use a valid accessToken in `dart pub publish` and `flutter pub publish`. 16 | 17 | ## Usage 18 | 19 | ### Overview 20 | 21 | unpub is using Google OAuth2 by default. There's two situations where the unpub_auth can be used. 22 | 23 | - Login locally, and publish pub packages locally. 24 | 1. Call `unpub_auth login` when you first use it, and it will save credentials locally. 25 | 2. Before calling `dart pub publish` or `flutter pub publish`, call `unpub_auth get | dart pub token add ` 26 | 27 | - Login locally, and publish pub packages from CI/CD. 28 | On CI/CD host device, you may not have opportunity to call `unpub_auth login`, so you can use `unpub_auth migrate` to migrate the credentials file. 29 | 1. In local device, call `unpub_auth login` when you first use it, and it will save credentials locally. 30 | 2. Copy the credentials file which was generated in step 1 to CI/CD device. 31 | 3. In CI/CD device, call `unpub_auth migrate `, so the CI/CD will have the same credentials file. 32 | 4. In CI/CD device, before calling `dart pub publish` or `flutter pub publish`, call `unpub_auth get | dart pub token add ` 33 | 34 | ``` 35 | Usage: unpub_auth [arguments] 36 | 37 | Available commands: 38 | get Refresh and get accessToken. Must login first. 39 | login Login unpub_auth on Google APIs. 40 | logout Delete local credentials file. 41 | migrate Migrate existed credentials file from path. 42 | ``` 43 | 44 | ### Install and run 45 | 46 | ``` bash 47 | dart pub global activate unpub_auth # activate the cli app 48 | ``` 49 | 50 | ### Uninstall 51 | 52 | ``` bash 53 | dart pub global deactivate unpub_auth # deactivate the cli app 54 | ``` 55 | 56 | ### Get a token and export to Dart Client 57 | 58 | ``` bash 59 | unpub_auth get | dart pub token add 60 | ``` 61 | 62 | **Please call `unpub_auth login` first before you run the `unpub_auth get` if you never login in 'terminal'.** 63 | 64 | ## Develop and debug locally 65 | 66 | ``` bash 67 | dart pub global activate --source path ./ # activate the cli app 68 | unpub_auth # run it 69 | ``` 70 | -------------------------------------------------------------------------------- /unpub_auth/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /unpub_auth/bin/unpub_auth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:console/console.dart'; 5 | import 'package:unpub_auth/unpub_auth.dart' as unpub_auth; 6 | import 'package:unpub_auth/utils.dart'; 7 | 8 | void main(List arguments) async { 9 | final parser = ArgParser(); 10 | parser.addCommand('login'); 11 | parser.addCommand('logout'); 12 | parser.addCommand('migrate'); 13 | parser.addCommand('get'); 14 | 15 | final result = parser.parse(arguments); 16 | 17 | unpub_auth.Flow flow = unpub_auth.Flow.getToken; 18 | 19 | Object? subArgs; 20 | 21 | switch (result.command?.name) { 22 | case 'login': 23 | flow = unpub_auth.Flow.login; 24 | break; 25 | case 'logout': 26 | flow = unpub_auth.Flow.logout; 27 | break; 28 | case 'migrate': 29 | flow = unpub_auth.Flow.migrate; 30 | if (result.command?.arguments.length != 1) { 31 | Utils.stdoutPrint("unpub_auth migrate need a path argument"); 32 | exit(1); 33 | } 34 | subArgs = result.command?.arguments.first; 35 | break; 36 | case 'get': 37 | flow = unpub_auth.Flow.getToken; 38 | break; 39 | default: 40 | stdout.write(format(''' 41 | An auth tool for unpub. unpub is using Google OAuth2 by default. There's two situations where the unpub_auth can be used. 42 | 43 | {@yellow}1. Login locally, and publish pub packages locally.{@end} 44 | {@blue}step 1.{@end} Call `unpub_auth login` when you first use it, and it will save credentials locally. 45 | {@blue}step 2.{@end} Before calling `dart pub publish` or `flutter pub publish`, call `unpub_auth get | dart pub token add ` 46 | 47 | {@yellow}2. Login locally, and publish pub packages from CI/CD.{@end} 48 | {@yellow} On CI/CD host device, you may not have opportunity to call `unpub_auth login`, so you can use `unpub_auth migrate` to migrate the credentials file.{@end} 49 | {@blue}step 1.{@end} In local device, call `unpub_auth login` when you first use it, and it will save credentials locally. 50 | {@blue}step 2.{@end} Copy the credentials file which was generated in step 1 to CI/CD device. 51 | {@blue}step 3.{@end} In CI/CD device, call `unpub_auth migrate `, so the CI/CD will have the same credentials file. 52 | {@blue}step 4.{@end} In CI/CD device, before calling `dart pub publish` or `flutter pub publish`, call `unpub_auth get | dart pub token add ` 53 | 54 | Usage: {@green}unpub_auth [arguments]{@end} 55 | 56 | Available commands: 57 | {@green}get{@end} Refresh and get a new accessToken. Must login first. 58 | {@green}login{@end} Login unpub_auth on Google APIs. 59 | {@green}logout{@end} Delete local credentials file. 60 | {@green}migrate{@end} {@green}{@end} Migrate existed credentials file from path. 61 | ''')); 62 | exit(0); 63 | } 64 | 65 | await unpub_auth.run(flow: flow, args: subArgs); 66 | exit(0); 67 | } 68 | -------------------------------------------------------------------------------- /unpub_auth/lib/credentials_ext.dart: -------------------------------------------------------------------------------- 1 | import 'package:oauth2/oauth2.dart'; 2 | 3 | extension Ext on Credentials { 4 | bool isValid() => refreshToken != null && refreshToken!.isNotEmpty; 5 | } 6 | -------------------------------------------------------------------------------- /unpub_auth/lib/unpub_auth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:shelf/shelf.dart' as shelf; 6 | import 'package:shelf/shelf_io.dart' as shelf_io; 7 | import 'package:oauth2/oauth2.dart' as oauth2; 8 | import 'package:http/http.dart' as http; 9 | 10 | import 'utils.dart'; 11 | import 'credentials_ext.dart'; 12 | 13 | const _tokenEndpoint = 'https://oauth2.googleapis.com/token'; 14 | const _authEndpoint = 'https://accounts.google.com/o/oauth2/auth'; 15 | const _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email']; 16 | 17 | get _identifier => utf8.decode(base64.decode( 18 | r'NDY4NDkyNDU2MjM5LTJja2wxdTB1dGloOHRzZWtnMGxpZ2NpY2VqYm8wbnZkLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t')); 19 | get _secret => utf8 20 | .decode(base64.decode(r'R09DU1BYLUxHMWZTV052UjA0S0NrWVZRMTVGS3J1cGJ5bFk=')); 21 | 22 | enum Flow { 23 | login, 24 | logout, 25 | migrate, 26 | getToken, 27 | } 28 | 29 | Future run({Flow flow = Flow.getToken, Object? args}) async { 30 | switch (flow) { 31 | case Flow.login: 32 | await _goAuth(); 33 | break; 34 | case Flow.logout: 35 | await removeCredentialsFromLocal(); 36 | break; 37 | case Flow.migrate: 38 | await migrate(args); 39 | break; 40 | case Flow.getToken: 41 | await getToken(); 42 | break; 43 | } 44 | } 45 | 46 | Future migrate(Object? args) async { 47 | if (args == null || args is! String) { 48 | Utils.stdoutPrint("$args is invalid"); 49 | exit(1); 50 | } 51 | if (File(args).existsSync() == false) { 52 | Utils.stdoutPrint("$args is not exist."); 53 | exit(1); 54 | } 55 | 56 | final isValid = 57 | oauth2.Credentials.fromJson(await File(args).readAsString()).isValid(); 58 | if (isValid) { 59 | await File(args).copy(Utils.credentialsFilePath); 60 | Utils.stdoutPrint( 61 | 'Migrate from $args success.\nNew credentials file is saved at ${Utils.credentialsFilePath}'); 62 | return; 63 | } 64 | } 65 | 66 | Future getToken() async { 67 | final credentials = await readCredentialsFromLocal(); 68 | 69 | if (credentials?.isValid() ?? false) { 70 | /// unpub-credentials.json is valid. 71 | /// Refresh and write it to file. 72 | await refreshCredentials(credentials!); 73 | } else { 74 | /// unpub-credentials.json is not exist or invalid. 75 | /// We should get a new Credentials file. 76 | Utils.stdoutPrint('${Utils.credentialsFilePath} is not found or invalid.' 77 | '\nPlease call unpub_auth login first.'); 78 | exit(1); 79 | } 80 | return; 81 | } 82 | 83 | Future _goAuth() async { 84 | final client = await clientWithAuthorization(); 85 | writeNewCredentials(client.credentials); 86 | Utils.stdoutPrint(client.credentials.accessToken); 87 | } 88 | 89 | /// Write the new credentials file to unpub-credentials.json 90 | void writeNewCredentials(oauth2.Credentials credentials) { 91 | File(Utils.credentialsFilePath).writeAsStringSync(credentials.toJson()); 92 | } 93 | 94 | /// Refresh `accessToken` of credentials 95 | Future refreshCredentials(oauth2.Credentials credentials) async { 96 | final client = oauth2.Client( 97 | oauth2.Credentials.fromJson(credentials.toJson()), 98 | identifier: _identifier, 99 | secret: _secret, onCredentialsRefreshed: (credential) async { 100 | writeNewCredentials(credential); 101 | }); 102 | await client.refreshCredentials(); 103 | Utils.stdoutPrint(client.credentials.accessToken); 104 | } 105 | 106 | /// Create a client with authorization. 107 | Future clientWithAuthorization() async { 108 | final grant = oauth2.AuthorizationCodeGrant( 109 | _identifier, Uri.parse(_authEndpoint), Uri.parse(_tokenEndpoint), 110 | secret: _secret, basicAuth: false, httpClient: http.Client()); 111 | 112 | final completer = Completer(); 113 | 114 | final server = await Utils.bindServer('localhost', 43230); 115 | shelf_io.serveRequests(server, (request) { 116 | if (request.url.path == 'authorized') { 117 | /// That's safe. 118 | /// see [dart-lang/pub/lib/src/oauth2.dart#L238:L240](https://github.com/dart-lang/pub/blob/400f21e9883ce6555b66d3ef82f0b732ba9b9fc8/lib/src/oauth2.dart#L238:L240) 119 | server.close(); 120 | return shelf.Response.ok(r'unpub Authorized Successfully.'); 121 | } 122 | 123 | if (request.url.path.isNotEmpty) { 124 | /// Forbid all other requests. 125 | return shelf.Response.notFound('Invalid URI.'); 126 | } 127 | 128 | Utils.stdoutPrint('Authorization received, processing...'); 129 | 130 | /// Redirect to authorized page. 131 | final resp = 132 | shelf.Response.found('http://localhost:${server.port}/authorized'); 133 | 134 | completer.complete( 135 | grant.handleAuthorizationResponse(Utils.queryToMap(request.url.query))); 136 | 137 | return resp; 138 | }); 139 | 140 | final authUrl = grant 141 | .getAuthorizationUrl(Uri.parse('http://localhost:${server.port}'), 142 | scopes: _scopes) 143 | .toString() + 144 | '&access_type=offline&approval_prompt=force'; 145 | Utils.stdoutPrint( 146 | 'unpub needs your authorization to upload packages on your behalf.\n' 147 | 'In a web browser, go to $authUrl\n' 148 | 'Then click "Allow access".\n\n' 149 | 'Waiting for your authorization...'); 150 | 151 | var client = await completer.future; 152 | Utils.stdoutPrint('Successfully authorized.\n'); 153 | return client; 154 | } 155 | 156 | /// Read credential file from local path. 157 | Future readCredentialsFromLocal() async { 158 | final credentialFile = File(Utils.credentialsFilePath); 159 | 160 | final exists = await credentialFile.exists(); 161 | if (!exists) { 162 | Utils.stdoutPrint('${Utils.credentialsFilePath} is not exist.\n' 163 | 'Please run `unpub_auth login` first'); 164 | return null; 165 | } 166 | 167 | final fileContent = await credentialFile.readAsString(); 168 | 169 | return oauth2.Credentials.fromJson(fileContent); 170 | } 171 | 172 | /// Remove credential file from local path. 173 | Future removeCredentialsFromLocal() async { 174 | await File(Utils.credentialsFilePath).delete(); 175 | Utils.stdoutPrint('${Utils.credentialsFilePath} has been deleted.'); 176 | } 177 | -------------------------------------------------------------------------------- /unpub_auth/lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:http_multi_server/http_multi_server.dart'; 3 | import 'package:path/path.dart' as path; 4 | 5 | class Utils { 6 | static bool _verbose = false; 7 | static void enableVerbose() => _verbose = true; 8 | static bool _silence = false; 9 | static void enableSilence() => _silence = true; 10 | 11 | static final credentialsFilePath = 12 | path.join(Utils.dartConfigDir, r'unpub-credentials.json'); 13 | 14 | /// The location for dart-specific configuration. 15 | static final String dartConfigDir = () { 16 | String? configDir; 17 | if (Platform.isLinux) { 18 | configDir = Platform.environment['XDG_CONFIG_HOME'] ?? 19 | path.join(Platform.environment['HOME']!, '.config'); 20 | } else if (Platform.isWindows) { 21 | configDir = Platform.environment['APPDATA']!; 22 | } else if (Platform.isMacOS) { 23 | configDir = path.join( 24 | Platform.environment['HOME']!, 'Library', 'Application Support'); 25 | } else { 26 | configDir = path.join(Platform.environment['HOME'] ?? '', '.config'); 27 | } 28 | final p = path.join(configDir, r'unpub-auth'); 29 | Directory(p).createSync(); 30 | return p; 31 | }(); 32 | 33 | static Future bindServer(String host, int port) async { 34 | var server = host == 'localhost' 35 | ? await HttpMultiServer.loopback(port) 36 | : await HttpServer.bind(host, port); 37 | server.autoCompress = true; 38 | return server; 39 | } 40 | 41 | static Map queryToMap(String queryList) { 42 | var map = {}; 43 | for (var pair in queryList.split('&')) { 44 | var split = _split(pair, '='); 45 | if (split.isEmpty) continue; 46 | var key = _urlDecode(split[0]); 47 | var value = split.length > 1 ? _urlDecode(split[1]) : ''; 48 | map[key] = value; 49 | } 50 | return map; 51 | } 52 | 53 | static String _urlDecode(String encoded) => 54 | Uri.decodeComponent(encoded.replaceAll('+', ' ')); 55 | 56 | static List _split(String toSplit, String pattern) { 57 | if (toSplit.isEmpty) return []; 58 | 59 | var index = toSplit.indexOf(pattern); 60 | if (index == -1) return [toSplit]; 61 | return [ 62 | toSplit.substring(0, index), 63 | toSplit.substring(index + pattern.length) 64 | ]; 65 | } 66 | 67 | static void stdoutPrint(Object? object) => stdout.write(object); 68 | } 69 | -------------------------------------------------------------------------------- /unpub_auth/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: unpub_auth 2 | description: An auth tool for unpub. 3 | version: 0.1.0+3 4 | homepage: https://github.com/bytedance/unpub 5 | 6 | environment: 7 | sdk: '>=2.15.0 <3.0.0' 8 | 9 | executables: 10 | unpub_auth: 11 | 12 | dependencies: 13 | collection: ^1.15.0 14 | path: ^1.8.1 15 | http: ^0.13.4 16 | shelf: ^1.2.0 17 | http_multi_server: ^3.0.1 18 | oauth2: ^2.0.0 19 | args: ^2.3.0 20 | console: ^4.1.0 21 | 22 | dev_dependencies: 23 | lints: ^1.0.0 24 | test: ^1.16.0 25 | -------------------------------------------------------------------------------- /unpub_auth/unpub_auth.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /unpub_aws/.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 | -------------------------------------------------------------------------------- /unpub_aws/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | - Initial release 4 | -------------------------------------------------------------------------------- /unpub_aws/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 | -------------------------------------------------------------------------------- /unpub_aws/README.md: -------------------------------------------------------------------------------- 1 | # unpub_aws 2 | 3 | A collection of modules to use for deploying unpub into AWS infrastructure. 4 | 5 | # Available Features 6 | ### 1. [S3 File Storage](#s3-file-storage) 7 | 8 | 9 | ## S3 File Storage 10 | 11 | Use AWS S3 or another S3 API compatible endpoint as your file storage. 12 | 13 | ```dart 14 | import 'package:unpub/unpub.dart' as unpub; 15 | import 'package:unpub/' as unpub_aws; 16 | 17 | var app = unpub.App( 18 | // ... 19 | packageStore: unpub.S3Store('your-bucket-name'), 20 | ); 21 | ``` 22 | 23 | ### What you need: 24 | - An S3 bucket created in AWS 25 | - AWS access credentials 26 | 27 | Authentication for AWS can be handled in 1 of 2 ways: Environment variables or during the `S3Store` class construction. 28 | 29 | #### Environment Variables 30 | ```dotenv 31 | AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxx 32 | AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 33 | AWS_DEFAULT_REGION=us-west-2 34 | AWS_S3_ENDPOINT=s3.amazonaws.com 35 | ``` 36 | 37 | 38 | Kitchen Sink Example: 39 | 40 | ```dart 41 | import 'package:mongo_dart/mongo_dart.dart'; 42 | import 'package:unpub/unpub.dart' as unpub; 43 | import 'package:unpub_aws/src/aws_credentials.dart'; 44 | import 'package:unpub_aws/unpub_aws.dart' as unpub_aws; 45 | 46 | main(List args) async { 47 | final db = Db('mongodb://localhost:27017/dart_pub'); 48 | await db.open(); // make sure the MongoDB connection opened 49 | 50 | final app = unpub.App( 51 | metaStore: unpub.MongoStore(db), 52 | packageStore: unpub_aws.S3Store('my-bucket-name', 53 | 54 | // We attempt to find region from AWS_DEFAULT_REGION. If one is not 55 | // available or provided an Argument error will be thrown. 56 | region: 'us-east-1', 57 | 58 | // Provide a different S3 compatible endpoint. 59 | endpoint: 'aws-alternative.example.com', 60 | 61 | // By default packages are sorted into folders in s3 like this. 62 | // Pass in an alternative if needed. 63 | getObjectPath: (String name, String version) => '$name/$name-$version.tar.gz', 64 | 65 | // You can provide credentials manually but... 66 | // don't be bad at security populate env vars instead... 67 | // 68 | // AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx 69 | // AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 70 | credentials: AwsCredentials( 71 | awsAccessKeyId: '', 72 | awsSecretAccessKey: '')), 73 | ); 74 | 75 | final server = await app.serve('0.0.0.0', 4000); 76 | print('Serving at http://${server.address.host}:${server.port}'); 77 | } 78 | 79 | ``` 80 | -------------------------------------------------------------------------------- /unpub_aws/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This docker-compose file should be used for tests 2 | 3 | version: '3.1' 4 | 5 | services: 6 | 7 | mongo: 8 | image: mongo:4.2 9 | restart: always 10 | ports: 11 | - "27017:27017" 12 | environment: 13 | MONGO_INITDB_DATABASE: dart_pub_test 14 | 15 | mock_s3: 16 | image: adobe/s3mock 17 | environment: 18 | initialBuckets: 'dart-pub-test' 19 | ports: 20 | - "9090:9090" 21 | - "9191:9191" 22 | 23 | # mongo-express: 24 | # image: mongo-express:0.49.0 25 | # restart: always 26 | # ports: 27 | # - "8081:8081" 28 | # environment: 29 | # ME_CONFIG_MONGODB_ADMINUSERNAME: root 30 | # ME_CONFIG_MONGODB_ADMINPASSWORD: example 31 | # ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/ 32 | 33 | -------------------------------------------------------------------------------- /unpub_aws/example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart/mongo_dart.dart'; 2 | import 'package:unpub/unpub.dart' as unpub; 3 | import 'package:unpub_aws/unpub_aws.dart' as unpub_aws; 4 | 5 | main(List args) async { 6 | final db = Db('mongodb://localhost:27017/dart_pub_test'); 7 | await db.open(); // make sure the MongoDB connection opened 8 | 9 | final app = unpub.App( 10 | metaStore: unpub.MongoStore(db), 11 | packageStore: unpub_aws.S3Store('my-bucket-name', 12 | 13 | // We attempt to find region from AWS_DEFAULT_REGION. If one is not 14 | // available or provided an Argument error will be thrown. 15 | region: 'us-east-1', 16 | 17 | // Provide a different S3 compatible endpoint. 18 | endpoint: 'aws-alternative.example.com', 19 | 20 | // By default packages are sorted into folders in s3 like this. 21 | // Pass in an alternative if needed. 22 | getObjectPath: (String name, String version) => '$name/$name-$version.tar.gz', 23 | 24 | // You can provide credentials manually but... 25 | // Don't be bad at security populate env vars instead... 26 | // AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxx 27 | // AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 28 | credentials: unpub_aws.AwsCredentials( 29 | awsAccessKeyId: '', 30 | awsSecretAccessKey: '', 31 | awsSessionToken: '')), 32 | ); 33 | 34 | final server = await app.serve('0.0.0.0', 4000); 35 | print('Serving at http://${server.address.host}:${server.port}'); 36 | } 37 | -------------------------------------------------------------------------------- /unpub_aws/lib/core/aws_credentials.dart: -------------------------------------------------------------------------------- 1 | import 'dart:cli'; 2 | import 'dart:io'; 3 | import 'dart:convert'; 4 | 5 | import 'package:http/http.dart' as http; 6 | 7 | class AwsCredentials { 8 | String? awsAccessKeyId; 9 | String? awsSecretAccessKey; 10 | String? awsSessionToken; 11 | Map? environment; 12 | Map? containerCredentials; 13 | 14 | AwsCredentials( 15 | {this.awsAccessKeyId, 16 | this.awsSecretAccessKey, 17 | this.awsSessionToken, 18 | this.environment, 19 | this.containerCredentials}) { 20 | 21 | final env = environment ?? Platform.environment; 22 | environment ??= Platform.environment; 23 | awsAccessKeyId = awsAccessKeyId ?? env['AWS_ACCESS_KEY_ID']; 24 | awsSecretAccessKey = awsSecretAccessKey ?? env['AWS_SECRET_ACCESS_KEY']; 25 | 26 | var isInContainer = env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']; 27 | 28 | if ((isInContainer != null || containerCredentials != null) && 29 | (awsAccessKeyId == null && awsSecretAccessKey == null)) { 30 | var data = containerCredentials ?? waitFor(getContainerCredentials(env)); 31 | if (data != null) { 32 | awsAccessKeyId = data['AccessKeyId']; 33 | awsSecretAccessKey = data['SecretAccessKey']; 34 | awsSessionToken = data['Token']; 35 | } 36 | } 37 | 38 | if (awsAccessKeyId == null || awsSecretAccessKey == null) { 39 | throw ArgumentError( 40 | 'You must provide a valid Access Key and Secret for AWS.'); 41 | } 42 | } 43 | 44 | Future?> getContainerCredentials( 45 | Map environment) async { 46 | try { 47 | var relativeUri = 48 | environment['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] ?? ''; 49 | var url = Uri.parse('http://169.254.170.2$relativeUri'); 50 | var response = await http.read(url); 51 | return json.decode(response); 52 | } catch (e) { 53 | print('failed to get container credentials.'); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /unpub_aws/lib/s3/s3_file_store.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:cli'; 3 | import 'dart:io'; 4 | 5 | import 'package:minio/minio.dart'; 6 | import 'package:unpub/unpub.dart'; 7 | import 'package:unpub_aws/core/aws_credentials.dart'; 8 | 9 | /// Use an AWS S3 Bucket as a package store 10 | class S3Store extends PackageStore { 11 | String Function(String name, String version)? getObjectPath; 12 | 13 | String bucketName; 14 | String? region; 15 | String? endpoint; 16 | AwsCredentials? credentials; 17 | Minio? minio; 18 | Map? environment; 19 | 20 | S3Store(this.bucketName, 21 | {this.region, 22 | this.getObjectPath, 23 | this.endpoint, 24 | this.credentials, 25 | this.minio, this.environment}) { 26 | 27 | final env = environment ?? Platform.environment; 28 | 29 | // Check for env vars or container credentials if none were provided. 30 | credentials ??= AwsCredentials(environment: env); 31 | 32 | // Use a supplied minio instance or create a default 33 | minio ??= Minio( 34 | endPoint: endpoint ?? env['AWS_S3_ENDPOINT'] ?? 's3.amazonaws.com', 35 | region: region ?? env['AWS_DEFAULT_REGION'], 36 | accessKey: credentials!.awsAccessKeyId ?? '', 37 | secretKey: credentials!.awsSecretAccessKey ?? '', 38 | ); 39 | 40 | // Check for a region or default region which is required 41 | if (region == null && 42 | (env['AWS_DEFAULT_REGION'] == null || 43 | env['AWS_DEFAULT_REGION']!.isEmpty)) { 44 | throw ArgumentError('Could not determine a default region for aws.'); 45 | } 46 | } 47 | 48 | String _getObjectKey(String name, String version) { 49 | return getObjectPath?.call(name, version) ?? '$name/$name-$version.tar.gz'; 50 | } 51 | 52 | @override 53 | Future upload(String name, String version, List content) async { 54 | await minio!.putObject( 55 | bucketName, _getObjectKey(name, version), Stream.value(content)); 56 | } 57 | 58 | @override 59 | Stream> download(String name, String version) { 60 | return waitFor(minio!.getObject(bucketName, _getObjectKey(name, version))); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /unpub_aws/lib/unpub_aws.dart: -------------------------------------------------------------------------------- 1 | export 's3/s3_file_store.dart'; 2 | export 'core/aws_credentials.dart'; 3 | -------------------------------------------------------------------------------- /unpub_aws/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: unpub_aws 2 | description: A collection of modules to use for deploying unpub into AWS infrastructure. 3 | version: 0.1.0 4 | homepage: https://github.com/bytedance/unpub 5 | environment: 6 | sdk: ">=2.12.0 <3.0.0" 7 | 8 | dependencies: 9 | unpub: ^2.0.0 10 | minio: ^3.0.0 11 | http: ^0.13.3 12 | 13 | dev_dependencies: 14 | lints: ^1.0.0 15 | test: ^1.17.12 16 | chunked_stream: ^1.4.1 17 | mockito: ^5.0.16 18 | build_runner: ^2.1.2 19 | path: ^1.8.0 20 | json_annotation: ^4.1.0 21 | # dependency_overrides: 22 | # unpub: 23 | # path: ../unpub 24 | -------------------------------------------------------------------------------- /unpub_aws/test/s3_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:chunked_stream/chunked_stream.dart'; 3 | import 'package:minio/minio.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:test/test.dart'; 6 | import 'package:unpub_aws/unpub_aws.dart'; 7 | 8 | void main() async { 9 | group('test s3 file store', () { 10 | late S3Store testS3Store; 11 | 12 | setUp(() async { 13 | testS3Store = S3Store('dart-pub-test', 14 | region: 'us-east-1', 15 | endpoint: 'localhost', 16 | getObjectPath: newFilePathFunc(), 17 | credentials: mockCreds, 18 | 19 | // Tests use an s3 mock docker image from docker-compose.yml 20 | minio: Minio( 21 | endPoint: 'localhost', 22 | accessKey: '', 23 | secretKey: '', 24 | useSSL: false, 25 | port: 9090, 26 | region: 'us-test-1', 27 | )); 28 | }); 29 | 30 | test('aws-credentials-manual', () async { 31 | var creds = AwsCredentials( 32 | awsAccessKeyId: 'specialKey', awsSecretAccessKey: 'specialSecret'); 33 | expect(creds.awsAccessKeyId, 'specialKey'); 34 | expect(creds.awsSecretAccessKey, 'specialSecret'); 35 | }); 36 | 37 | test('aws-credentials-env', () async { 38 | var credMap = { 39 | 'AWS_ACCESS_KEY_ID': 'special-key-id', 40 | 'AWS_SECRET_ACCESS_KEY': 'special-access-key', 41 | }; 42 | var creds = await AwsCredentials(environment: credMap); 43 | expect(creds.awsAccessKeyId, credMap['AWS_ACCESS_KEY_ID']); 44 | expect(creds.awsSecretAccessKey, credMap['AWS_SECRET_ACCESS_KEY']); 45 | }); 46 | 47 | test('aws-credentials-ecs-container-iam', () async { 48 | var credMap = { 49 | 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI': 'special-access-key', 50 | }; 51 | Map containerCredentials = { 52 | 'AccessKeyId': 'container-creds-key', 53 | 'SecretAccessKey': 'container-creds-secret', 54 | 'Token': 'container-creds-token' 55 | }; 56 | var creds = await AwsCredentials( 57 | environment: credMap, containerCredentials: containerCredentials); 58 | 59 | expect(creds.awsAccessKeyId, containerCredentials['AccessKeyId']); 60 | expect(creds.awsSecretAccessKey, containerCredentials['SecretAccessKey']); 61 | expect(creds.awsSessionToken, containerCredentials['Token']); 62 | }); 63 | 64 | test('upload-download-default-path', () async { 65 | await testS3Store.upload('test_package', '1.0.0', testPackageData); 66 | var pkg1 = 67 | await readByteStream(testS3Store.download('test_package', '1.0.0')); 68 | expect(pkg1, testPackageData); 69 | }); 70 | 71 | test('upload-download-custom-path', () async { 72 | expect(testS3Store.getObjectPath!.call('test_package', '1.0.0'), 73 | newFilePathFunc().call('test_package', '1.0.0')); 74 | expect(testS3Store.getObjectPath!.call('test_package2', '2.0.0'), 75 | newFilePathFunc().call('test_package2', '2.0.0')); 76 | 77 | await testS3Store.upload('test_package', '1.0.0', testPackageData2); 78 | var pkg2 = 79 | await readByteStream(testS3Store.download('test_package', '1.0.0')); 80 | expect(pkg2, testPackageData2, reason: 'tar.gz content did not match'); 81 | }); 82 | 83 | test('require-default-aws-region', () async { 84 | var storePass = 85 | S3Store('dart-pub-test', region: 'us-east-1', credentials: mockCreds); 86 | expect(storePass.region, 'us-east-1'); 87 | 88 | // Don't run tests with AWS environment set variables please 89 | expect(Platform.environment['AWS_DEFAULT_REGION'], null); 90 | try { 91 | S3Store('dart_pub_test', credentials: mockCreds); 92 | } on ArgumentError catch (e) { 93 | expect(e.message, 'Could not determine a default region for aws.'); 94 | } 95 | }); 96 | }); 97 | } 98 | 99 | final mockCreds = AwsCredentials(awsAccessKeyId: '', awsSecretAccessKey: ''); 100 | 101 | //test gzip data 102 | const testPackageData = [ 103 | 0x8b, 0x1f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, // 104 | 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // 105 | ]; 106 | 107 | // This is a tar.gz file with a `hello.txt` file that has `hello` written inside 108 | const testPackageData2 = [31,139,8,0,4,81,79,97,0,3,237,207,73,10,128,48,12,5, 109 | 80,215,158,34,39,144,84,99,123,30,209,138,139,98,193,214,233,246,142,116,37, 110 | 238,84,132,188,205,39,16,194,79,163,141,177,137,159,124,244,28,68,148,68,176, 111 | 165,82,98,79,76,143,249,36,65,144,72,51,153,99,38,17,80,144,162,60,2,124,176, 112 | 83,208,59,95,116,107,149,210,26,237,188,30,116,235,108,123,177,183,174,213, 113 | 245,205,157,227,17,8,249,19,141,29,171,57,254,186,5,99,140,177,183,45,78,193, 114 | 149,248,0,8,0,0]; 115 | 116 | String Function(String, String) newFilePathFunc() { 117 | return (String package, String version) { 118 | var grp = package[0]; 119 | var subgrp = package.substring(0, 2); 120 | return path.join('packages', grp, subgrp, package, 'versions', 121 | '$package-$version.tar.gz'); 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /unpub_web/.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_web/README.md: -------------------------------------------------------------------------------- 1 | # unpub_web 2 | 3 | A web app that uses [AngularDart](https://webdev.dartlang.org/angular) and 4 | [AngularDart Components](https://webdev.dartlang.org/components). 5 | 6 | Created from templates made available by Stagehand under a BSD-style 7 | [license](https://github.com/dart-lang/stagehand/blob/master/LICENSE). 8 | -------------------------------------------------------------------------------- /unpub_web/lib/app_component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | /* This is equivalent of the 'body' selector of a page. */ 3 | } 4 | 5 | .home-banner { 6 | padding-bottom: 20px; 7 | } 8 | -------------------------------------------------------------------------------- /unpub_web/lib/app_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html' as html; 2 | import 'package:angular/angular.dart'; 3 | import 'package:angular_router/angular_router.dart'; 4 | import 'package:angular_forms/angular_forms.dart'; 5 | import 'package:unpub_web/src/routes.dart'; 6 | import 'app_service.dart'; 7 | 8 | @Component( 9 | selector: 'my-app', 10 | styleUrls: ['app_component.css'], 11 | templateUrl: 'app_component.html', 12 | directives: [routerDirectives, coreDirectives, formDirectives], 13 | exports: [RoutePaths, Routes], 14 | providers: [ClassProvider(AppService)], 15 | ) 16 | class AppComponent { 17 | final AppService appService; 18 | final Router _router; 19 | AppComponent(this.appService, this._router); 20 | 21 | submit() async { 22 | if (appService.keyword == '') { 23 | return html.window.alert('keyword empty'); 24 | } 25 | var result = await _router.navigate(RoutePaths.list.toUrl(), 26 | NavigationParams(queryParameters: {'q': appService.keyword})); 27 | // print(result); 28 | } 29 | 30 | String get homeUrl => RoutePaths.home.toUrl(); 31 | bool get loading => appService.loading; 32 | } 33 | -------------------------------------------------------------------------------- /unpub_web/lib/app_component.html: -------------------------------------------------------------------------------- 1 |
2 | 18 |
19 |
20 |
21 |
22 | 33 |
34 |
35 |
36 |
37 | 38 |
39 | 47 | -------------------------------------------------------------------------------- /unpub_web/lib/app_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:http/http.dart' as http; 3 | import 'package:angular/core.dart'; 4 | import 'package:unpub_web/constants.dart'; 5 | import 'src/routes.dart'; 6 | import 'package:unpub_api/models.dart'; 7 | 8 | class PackageNotExistsException implements Exception { 9 | final String message; 10 | PackageNotExistsException(this.message); 11 | } 12 | 13 | @Injectable() 14 | class AppService { 15 | bool loading = false; 16 | String keyword = ''; 17 | 18 | void setLoading(bool value) { 19 | loading = value; 20 | } 21 | 22 | Future _fetch(String path, 23 | [Map queryParameters = const {}]) async { 24 | queryParameters.entries 25 | .where((entry) => entry.value == null) 26 | .toList() 27 | .forEach((entry) => queryParameters.remove(entry.key)); 28 | 29 | var baseUrl = isProduction ? '' : 'http://localhost:4000'; 30 | var uri = Uri.parse(baseUrl).replace( 31 | path: path, 32 | queryParameters: queryParameters.map((k, v) => MapEntry(k, v.toString())), 33 | ); 34 | var res = await http.get(uri); 35 | var data = json.decode(res.body); 36 | 37 | if (data['error'] != null) { 38 | var error = data['error'] as String; 39 | if (error.contains('package not exists')) { 40 | throw PackageNotExistsException(error); 41 | } 42 | throw error; 43 | } 44 | 45 | return data['data']; 46 | } 47 | 48 | Future fetchPackages( 49 | {int size, int page, String sort, String q}) async { 50 | var res = await _fetch( 51 | '/webapi/packages', {'size': size, 'page': page, 'sort': sort, 'q': q}); 52 | return ListApi.fromJson(res); 53 | } 54 | 55 | Future fetchPackage(String name, String version) async { 56 | version = version ?? 'latest'; 57 | var res = await _fetch('/webapi/package/$name/$version'); 58 | return WebapiDetailView.fromJson(res); 59 | } 60 | 61 | getDetailUrl(package) { 62 | return RoutePaths.detail.toUrl(parameters: {'name': package['name']}); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /unpub_web/lib/constants.dart: -------------------------------------------------------------------------------- 1 | const isProduction = true; 2 | -------------------------------------------------------------------------------- /unpub_web/lib/src/detail_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | import 'package:angular/angular.dart'; 4 | import 'package:angular_router/angular_router.dart'; 5 | import 'package:markdown/markdown.dart'; 6 | import 'package:unpub_web/app_service.dart'; 7 | import 'routes.dart'; 8 | import 'package:unpub_api/models.dart'; 9 | 10 | // Allow all url 11 | // https://stackoverflow.com/questions/18867266/dart-removing-disallowed-attribute-after-editor-upgraded 12 | class _MyUriPolicy implements UriPolicy { 13 | bool allowsUri(String uri) => true; 14 | } 15 | 16 | final _myUriPolify = _MyUriPolicy(); 17 | 18 | final NodeValidatorBuilder _htmlValidator = NodeValidatorBuilder.common() 19 | ..allowElement('a', attributes: ['href'], uriPolicy: _myUriPolify) 20 | ..allowElement('img', uriAttributes: ['src'], uriPolicy: _myUriPolify); 21 | 22 | @Component( 23 | selector: 'detail', 24 | templateUrl: 'detail_component.html', 25 | directives: [routerDirectives, coreDirectives], 26 | exports: [RoutePaths], 27 | styles: ['.not-exists { margin-top: 100px }'], 28 | pipes: [DatePipe], 29 | ) 30 | class DetailComponent implements OnInit, OnActivate { 31 | final AppService appService; 32 | DetailComponent(this.appService); 33 | 34 | WebapiDetailView package; 35 | String packageName; 36 | String packageVersion; 37 | int activeTab = 0; 38 | bool packageNotExists = false; 39 | 40 | String get readmeHtml => 41 | package.readme == null ? null : markdownToHtml(package.readme); 42 | 43 | String get changelogHtml => 44 | package.changelog == null ? null : markdownToHtml(package.changelog); 45 | 46 | String get pubDevLink { 47 | var url = 'https://pub.dev/packages/$packageName'; 48 | if (packageVersion != null) { 49 | url += '/versions/$packageVersion'; 50 | } 51 | return url; 52 | } 53 | 54 | @override 55 | Future ngOnInit() async { 56 | activeTab = 0; 57 | } 58 | 59 | @override 60 | void onActivate(_, RouterState current) async { 61 | final name = current.parameters['name']; 62 | final version = current.parameters['version']; 63 | 64 | if (name != null) { 65 | packageName = name; 66 | packageVersion = version; 67 | appService.setLoading(true); 68 | try { 69 | package = await appService.fetchPackage(name, version); 70 | await Future.delayed(Duration(seconds: 0)); // Next tick 71 | querySelector('#readme') 72 | .setInnerHtml(readmeHtml, validator: _htmlValidator); 73 | querySelector('#changelog') 74 | .setInnerHtml(changelogHtml, validator: _htmlValidator); 75 | } on PackageNotExistsException { 76 | packageNotExists = true; 77 | } finally { 78 | appService.setLoading(false); 79 | } 80 | } 81 | } 82 | 83 | getListUrl(String q) { 84 | return RoutePaths.list.toUrl(queryParameters: {'q': q}); 85 | } 86 | 87 | getDetailUrl(String name, [String version]) { 88 | if (version == null) { 89 | return RoutePaths.detail.toUrl(parameters: {'name': name}); 90 | } else { 91 | return RoutePaths.detailVersion 92 | .toUrl(parameters: {'name': name, 'version': version}); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /unpub_web/lib/src/detail_component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ package.name }} {{ package.version }}

4 | 12 |
13 | 14 |
15 |
    16 |
  • 22 | README.md 23 |
  • 24 |
  • 30 | CHANGELOG.md 31 |
  • 32 | 33 |
  • 39 | Versions 40 |
  • 41 |
42 |
43 |
48 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 72 | 73 | 88 | 99 | 100 | 101 |
VersionUploadedDocumentationArchive
66 | {{ item.version }} 71 | {{ item.createdAt | date: "mediumDate" }} 74 | 81 | Go to the documentation of {{ package.name }} {{
 84 |                   item.version }} 86 | 87 | 89 | 93 | Download {{ package.name }} {{ item.version }} archive 97 | 98 |
102 |
103 |
104 | 105 | 164 |
165 |
166 | 167 |
168 |
169 |
This is not a private package, click link below to view it:
170 | {{ pubDevLink }} 173 |
174 |
175 | -------------------------------------------------------------------------------- /unpub_web/lib/src/home_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:angular_router/angular_router.dart'; 3 | import 'package:unpub_web/app_service.dart'; 4 | import 'routes.dart'; 5 | import 'package:unpub_api/models.dart'; 6 | 7 | @Component( 8 | selector: 'home', 9 | templateUrl: 'home_component.html', 10 | directives: [routerDirectives, coreDirectives], 11 | exports: [RoutePaths], 12 | ) 13 | class HomeComponent implements OnActivate { 14 | final AppService appService; 15 | 16 | ListApi data; 17 | HomeComponent(this.appService); 18 | 19 | @override 20 | void onActivate(RouterState previous, RouterState current) async { 21 | appService.setLoading(true); 22 | data = await appService.fetchPackages(size: 15); 23 | appService.setLoading(false); 24 | } 25 | 26 | getDetailUrl(ListApiPackage package) { 27 | return RoutePaths.detail.toUrl(parameters: {'name': package.name}); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /unpub_web/lib/src/home_component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | Top Dart packages 7 |

8 |
9 |
10 | 11 |
    12 |
  • 13 |

    14 | {{ package.name }} 15 |

    16 | 21 |

    {{ package.description }}

    22 |
  • 23 |
24 | 25 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /unpub_web/lib/src/list_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | import 'package:angular/angular.dart'; 4 | import 'package:angular_router/angular_router.dart'; 5 | import 'package:unpub_web/app_service.dart'; 6 | import 'routes.dart'; 7 | import 'package:unpub_api/models.dart'; 8 | 9 | @Component( 10 | selector: 'list', 11 | templateUrl: 'list_component.html', 12 | directives: [routerDirectives, coreDirectives], 13 | exports: [RoutePaths], 14 | pipes: [DatePipe], 15 | ) 16 | class ListComponent implements OnInit, OnActivate { 17 | final AppService appService; 18 | static final size = 10; 19 | 20 | String q; 21 | int currentPage; 22 | ListApi data; 23 | ListComponent(this.appService); 24 | 25 | int get pageCount => (data.count / size).ceil(); 26 | 27 | List get pages { 28 | if (data == null) return []; 29 | var leftCount = min(currentPage, 5); 30 | var rightCount = min(pageCount - currentPage, 5); 31 | var offset = max(currentPage - 5, 0); 32 | return List.generate(leftCount + rightCount + 1, (i) => i + offset); 33 | } 34 | 35 | @override 36 | Future ngOnInit() async {} 37 | 38 | @override 39 | void onActivate(RouterState previous, RouterState current) async { 40 | q = current.queryParameters['q']; 41 | currentPage = int.tryParse(current.queryParameters['page'] ?? '0') ?? 0; 42 | appService.setLoading(true); 43 | data = await appService.fetchPackages(size: size, page: currentPage, q: q); 44 | appService.setLoading(false); 45 | } 46 | 47 | getListUrl(int page) { 48 | Map queryParameters = {}; 49 | if (q != null) queryParameters['q'] = q; 50 | if (page > 0) queryParameters['page'] = page.toString(); 51 | 52 | return RoutePaths.list.toUrl(queryParameters: queryParameters); 53 | } 54 | 55 | getDetailUrl(ListApiPackage package) { 56 | return RoutePaths.detail.toUrl(parameters: {'name': package.name}); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /unpub_web/lib/src/list_component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ data.count }} results

3 | 4 |
    5 |
  • 6 |

    7 | {{ package.name }} 8 |

    9 |

    {{ package.description }}

    10 | 18 |
  • 19 |
20 | 37 |
38 | -------------------------------------------------------------------------------- /unpub_web/lib/src/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular_router/angular_router.dart'; 2 | 3 | import 'home_component.template.dart' as home_template; 4 | import 'list_component.template.dart' as list_template; 5 | import 'detail_component.template.dart' as detail_template; 6 | // import 'not_found_component.template.dart' as not_found_template; 7 | 8 | class RoutePaths { 9 | static final home = RoutePath(path: ''); 10 | static final list = RoutePath(path: 'packages'); 11 | static final detail = RoutePath(path: 'packages/:name'); 12 | static final detailVersion = 13 | RoutePath(path: 'packages/:name/versions/:version'); 14 | } 15 | 16 | class Routes { 17 | static final home = RouteDefinition( 18 | routePath: RoutePaths.home, 19 | component: home_template.HomeComponentNgFactory, 20 | ); 21 | static final list = RouteDefinition( 22 | routePath: RoutePaths.list, 23 | component: list_template.ListComponentNgFactory, 24 | ); 25 | static final detail = RouteDefinition( 26 | routePath: RoutePaths.detail, 27 | component: detail_template.DetailComponentNgFactory, 28 | ); 29 | static final detailVersion = RouteDefinition( 30 | routePath: RoutePaths.detailVersion, 31 | component: detail_template.DetailComponentNgFactory, 32 | ); 33 | 34 | static final all = [ 35 | home, 36 | list, 37 | detail, 38 | // RouteDefinition.redirect( 39 | // path: '', 40 | // redirectTo: RoutePaths.heroes.toUrl(), 41 | // ), 42 | // RouteDefinition( 43 | // path: '.*', 44 | // component: not_found_template.NotFoundComponentNgFactory, 45 | // ), 46 | ]; 47 | } 48 | -------------------------------------------------------------------------------- /unpub_web/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: unpub_web 2 | description: A web app that uses AngularDart Components 3 | version: 1.0.0 4 | publish_to: none 5 | environment: 6 | sdk: ">=2.3.0 <3.0.0" 7 | dependencies: 8 | angular: ^6.0.0 9 | angular_forms: ^3.0.0 10 | angular_router: ^2.0.0 11 | http: ^0.13.3 12 | markdown: ^3.0.0 13 | unpub_api: 14 | path: ../unpub/lib/unpub_api 15 | dev_dependencies: 16 | build_runner: ^1.11.1+1 17 | build_web_compilers: ^2.16.3 18 | -------------------------------------------------------------------------------- /unpub_web/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unpub 5 | 6 | 7 | 8 | 12 | 13 | 1300 | 1700 | 1701 | 1702 | 1703 | 1704 | Loading... 1705 | 1706 | 1707 | -------------------------------------------------------------------------------- /unpub_web/web/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:angular/angular.dart'; 2 | import 'package:angular_router/angular_router.dart'; 3 | import 'package:unpub_web/app_component.template.dart' as ng; 4 | import 'main.template.dart' as self; 5 | 6 | @GenerateInjector(routerProviders) 7 | final InjectorFactory injector = self.injector$Injector; 8 | 9 | void main() { 10 | runApp(ng.AppComponentNgFactory, createInjector: injector); 11 | } 12 | --------------------------------------------------------------------------------