This commit is contained in:
2025-07-05 19:58:34 +02:00
commit 153dc866ff
56 changed files with 20264 additions and 0 deletions

View File

@ -0,0 +1,21 @@
import 'dart:io';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:unpub/unpub.dart' as unpub;
Future<void> main(List<String> args) async {
final String port = Platform.environment['UNPUB_PORT']!;
final String dbUri = Platform.environment['UNPUB_DB_URI']!;
final String? proxyUri = Platform.environment['UNPUB_REVERSE_PROXY_URI'];
final Db db = Db(dbUri);
await db.open();
final unpub.App app = unpub.App(
metaStore: unpub.MongoStore(db),
packageStore: unpub.FileStore('/unpub-packages'),
proxy_origin: proxyUri != null ? Uri.parse(proxyUri) : null,
);
final server = await app.serve('0.0.0.0', int.parse(port));
print('Serving at http://${server.address.host}:${server.port}');
}

View File

@ -0,0 +1,60 @@
## 2.1.0
- https://github.com/bytedance/unpub/pull/85
- https://github.com/bytedance/unpub/pull/80
- https://github.com/bytedance/unpub/pull/70
- https://github.com/bytedance/unpub/pull/66
- https://github.com/bytedance/unpub/pull/60
- https://github.com/bytedance/unpub/pull/53
- https://github.com/bytedance/unpub/pull/51
- https://github.com/bytedance/unpub/pull/50
- https://github.com/bytedance/unpub/pull/48
- https://github.com/bytedance/unpub/pull/47
- https://github.com/bytedance/unpub/pull/38
- https://github.com/bytedance/unpub/pull/35
## 2.0.0
- Supports NNBD
- Fixes Web styles
## 1.2.1
## 1.2.0
- Supports mongodb pool connection
- Update web page styles
## 1.1.0
- Add badges for version and downloads
- Fix web page styles
## 1.0.0
## 0.4.0
## 0.3.0
## 0.2.2
## 0.2.1
## 0.2.0
- Refactor
- Semver whitelist
## 0.1.1
- Get email via Google APIs
- Upload validator
## 0.1.0
- `pub get`
- `pub publish` with permission check
## 0.0.1
- Initial version, created by Stagehand

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Rongjian Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,142 @@
# Unpub
[![pub](https://img.shields.io/pub/v/unpub.svg)](https://pub.dev/packages/unpub)
Unpub is a self-hosted private Dart Pub server for Enterprise, with a simple web interface to search and view packages information.
## Screenshots
![Screenshot](https://raw.githubusercontent.com/bytedance/unpub/master/assets/screenshot.png)
## Usage
### Command Line
```sh
pub global activate unpub
unpub --database mongodb://localhost:27017/dart_pub # Replace this with production database uri
```
Unpub use mongodb as meta information store and file system as package(tarball) store by default.
Dart API is also available for further customization.
### Dart API
```dart
import 'package:mongo_dart/mongo_dart.dart';
import 'package:unpub/unpub.dart' as unpub;
main(List<String> args) async {
final db = Db('mongodb://localhost:27017/dart_pub');
await db.open(); // make sure the MongoDB connection opened
final app = unpub.App(
metaStore: unpub.MongoStore(db),
packageStore: unpub.FileStore('./unpub-packages'),
);
final server = await app.serve('0.0.0.0', 4000);
print('Serving at http://${server.address.host}:${server.port}');
}
```
### Options
| Option | Description | Default |
| --- | --- | --- |
| `metaStore` (Required) | Meta information store | - |
| `packageStore` (Required) | Package(tarball) store | - |
| `upstream` | Upstream url | https://pub.dev |
| `googleapisProxy` | Http(s) proxy to call googleapis (to get uploader email) | - |
| `uploadValidator` | See [Package validator](#package-validator) | - |
### Usage behind reverse-proxy
Using unpub behind reverse proxy(nginx or another), ensure you have necessary headers
```sh
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Workaround for:
# Asynchronous error HttpException:
# Trying to set 'Transfer-Encoding: Chunked' on HTTP 1.0 headers
proxy_http_version 1.1;
```
### Package validator
Naming conflicts is a common issue for private registry. A reasonable solution is to add prefix to reduce conflict probability.
With `uploadValidator` you could check if uploaded package is valid.
```dart
var app = unpub.App(
// ...
uploadValidator: (Map<String, dynamic> pubspec, String uploaderEmail) {
// Only allow packages with some specified prefixes to be uploaded
var prefix = 'my_awesome_prefix_';
var name = pubspec['name'] as String;
if (!name.startsWith(prefix)) {
throw 'Package name should starts with $prefix';
}
// Also, you can check if uploader email is valid
if (!uploaderEmail.endsWith('@your-company.com')) {
throw 'Uploader email invalid';
}
}
);
```
### Customize meta and package store
Unpub is designed to be extensible. It is quite easy to customize your own meta store and package store.
```dart
import 'package:unpub/unpub.dart' as unpub;
class MyAwesomeMetaStore extends unpub.MetaStore {
// Implement methods of MetaStore abstract class
// ...
}
class MyAwesomePackageStore extends unpub.PackageStore {
// Implement methods of PackageStore abstract class
// ...
}
// Then use it
var app = unpub.App(
metaStore: MyAwesomeMetaStore(),
packageStore: MyAwesomePackageStore(),
);
```
#### Available Package Stores
1. [unpub_aws](https://github.com/bytedance/unpub/tree/master/unpub_aws): AWS S3 package store, maintained by [@CleanCode](https://github.com/Clean-Cole).
## Badges
| URL | Badge |
| --- | --- |
| `/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) |
| `/badge/d/{package_name}` | ![badge example](https://img.shields.io/static/v1?label=downloads&message=123&color=blue) |
## Alternatives
- [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.
- [pub_server](https://github.com/dart-lang/pub_server): An alpha version of pub server provided by Dart team.
## Credits
- [pub-dev](https://github.com/dart-lang/pub-dev): Web page styles are mostly imported from https://pub.dev directly.
- [shields](https://shields.io): Badges generation.
## License
MIT

View File

@ -0,0 +1,41 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:args/args.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:unpub/unpub.dart' as unpub;
main(List<String> args) async {
var parser = ArgParser();
parser.addOption('host', abbr: 'h', defaultsTo: '0.0.0.0');
parser.addOption('port', abbr: 'p', defaultsTo: '4000');
parser.addOption('database',
abbr: 'd', defaultsTo: 'mongodb://localhost:27017/dart_pub');
parser.addOption('proxy-origin', abbr: 'o', defaultsTo: '');
var results = parser.parse(args);
var host = results['host'] as String;
var port = int.parse(results['port'] as String);
var dbUri = results['database'] as String;
var proxy_origin = results['proxy-origin'] as String;
if (results.rest.isNotEmpty) {
print('Got unexpected arguments: "${results.rest.join(' ')}".\n\nUsage:\n');
print(parser.usage);
exit(1);
}
final db = Db(dbUri);
await db.open();
var baseDir = path.absolute('unpub-packages');
var app = unpub.App(
metaStore: unpub.MongoStore(db),
packageStore: unpub.FileStore(baseDir),
proxy_origin: proxy_origin.trim().isEmpty ? null : Uri.parse(proxy_origin)
);
var server = await app.serve(host, port);
print('Serving at http://${server.address.host}:${server.port}');
}

View File

@ -0,0 +1,15 @@
import 'package:mongo_dart/mongo_dart.dart';
import 'package:unpub/unpub.dart' as unpub;
main(List<String> args) async {
final db = Db('mongodb://localhost:27017/dart_pub');
await db.open(); // make sure the MongoDB connection opened
final app = unpub.App(
metaStore: unpub.MongoStore(db),
packageStore: unpub.FileStore('./unpub-packages'),
);
final server = await app.serve('0.0.0.0', 4000);
print('Serving at http://${server.address.host}:${server.port}');
}

View File

@ -0,0 +1,579 @@
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:googleapis/oauth2/v2.dart';
import 'package:mime/mime.dart';
import 'package:http_parser/http_parser.dart';
import 'package:shelf_cors_headers/shelf_cors_headers.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:pub_semver/pub_semver.dart' as semver;
import 'package:archive/archive.dart';
import 'package:unpub/src/models.dart';
import 'package:unpub/unpub_api/lib/models.dart';
import 'package:unpub/src/meta_store.dart';
import 'package:unpub/src/package_store.dart';
import 'utils.dart';
import 'static/index.html.dart' as index_html;
import 'static/main.dart.js.dart' as main_dart_js;
part 'app.g.dart';
class App {
static const proxyOriginHeader = "proxy-origin";
/// meta information store
final MetaStore metaStore;
/// package(tarball) store
final PackageStore packageStore;
/// upstream url, default: https://pub.dev
final String upstream;
/// http(s) proxy to call googleapis (to get uploader email)
final String? googleapisProxy;
final String? overrideUploaderEmail;
/// A forward proxy uri
final Uri? proxy_origin;
/// validate if the package can be published
///
/// for more details, see: https://github.com/bytedance/unpub#package-validator
final Future<void> Function(
Map<String, dynamic> pubspec, String uploaderEmail)? uploadValidator;
App({
required this.metaStore,
required this.packageStore,
this.upstream = 'https://pub.dev',
this.googleapisProxy,
this.overrideUploaderEmail,
this.uploadValidator,
this.proxy_origin,
});
static shelf.Response _okWithJson(Map<String, dynamic> data) =>
shelf.Response.ok(
json.encode(data),
headers: {
HttpHeaders.contentTypeHeader: ContentType.json.mimeType,
'Access-Control-Allow-Origin': '*'
},
);
static shelf.Response _successMessage(String message) => _okWithJson({
'success': {'message': message}
});
static shelf.Response _badRequest(String message,
{int status = HttpStatus.badRequest}) =>
shelf.Response(
status,
headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType},
body: json.encode({
'error': {'message': message}
}),
);
http.Client? _googleapisClient;
String _resolveUrl(shelf.Request req, String reference) {
if (proxy_origin != null) {
return proxy_origin!.resolve(reference).toString();
}
String? proxyOriginInHeader = req.headers[proxyOriginHeader];
if (proxyOriginInHeader != null) {
return Uri.parse(proxyOriginInHeader).resolve(reference).toString();
}
return req.requestedUri.resolve(reference).toString();
}
Future<String> _getUploaderEmail(shelf.Request req) async {
if (overrideUploaderEmail != null) return overrideUploaderEmail!;
var authHeader = req.headers[HttpHeaders.authorizationHeader];
if (authHeader == null) throw 'missing authorization header';
var token = authHeader.split(' ').last;
if (_googleapisClient == null) {
if (googleapisProxy != null) {
_googleapisClient = IOClient(HttpClient()
..findProxy = (url) => HttpClient.findProxyFromEnvironment(url,
environment: {"https_proxy": googleapisProxy!}));
} else {
_googleapisClient = http.Client();
}
}
var info =
await Oauth2Api(_googleapisClient!).tokeninfo(accessToken: token);
if (info.email == null) throw 'fail to get google account email';
return info.email!;
}
Future<HttpServer> serve([String host = '0.0.0.0', int port = 4000]) async {
var handler = const shelf.Pipeline()
.addMiddleware(corsHeaders())
.addMiddleware(shelf.logRequests())
.addHandler((req) async {
// Return 404 by default
// https://github.com/google/dart-neats/issues/1
var res = await router.call(req);
return res;
});
var server = await shelf_io.serve(handler, host, port);
return server;
}
Map<String, dynamic> _versionToJson(UnpubVersion item, shelf.Request req) {
var name = item.pubspec['name'] as String;
var version = item.version;
return {
'archive_url': _resolveUrl(req, '/packages/$name/versions/$version.tar.gz'),
'pubspec': item.pubspec,
'version': version,
};
}
bool isPubClient(shelf.Request req) {
var ua = req.headers[HttpHeaders.userAgentHeader];
print(ua);
return ua != null && ua.toLowerCase().contains('dart pub');
}
Router get router => _$AppRouter(this);
@Route.get('/api/packages/<name>')
Future<shelf.Response> getVersions(shelf.Request req, String name) async {
var package = await metaStore.queryPackage(name);
if (package == null) {
return shelf.Response.found(
Uri.parse(upstream).resolve('/api/packages/$name').toString());
}
package.versions.sort((a, b) {
return semver.Version.prioritize(
semver.Version.parse(a.version), semver.Version.parse(b.version));
});
var versionMaps = package.versions
.map((item) => _versionToJson(item, req))
.toList();
return _okWithJson({
'name': name,
'latest': versionMaps.last, // TODO: Exclude pre release
'versions': versionMaps,
});
}
@Route.get('/api/packages/<name>/versions/<version>')
Future<shelf.Response> getVersion(
shelf.Request req, String name, String version) async {
// Important: + -> %2B, should be decoded here
try {
version = Uri.decodeComponent(version);
} catch (err) {
print(err);
}
var package = await metaStore.queryPackage(name);
if (package == null) {
return shelf.Response.found(Uri.parse(upstream)
.resolve('/api/packages/$name/versions/$version')
.toString());
}
var packageVersion =
package.versions.firstWhereOrNull((item) => item.version == version);
if (packageVersion == null) {
return shelf.Response.notFound('Not Found');
}
return _okWithJson(_versionToJson(packageVersion, req));
}
@Route.get('/packages/<name>/versions/<version>.tar.gz')
Future<shelf.Response> download(
shelf.Request req, String name, String version) async {
var package = await metaStore.queryPackage(name);
if (package == null) {
return shelf.Response.found(Uri.parse(upstream)
.resolve('/packages/$name/versions/$version.tar.gz')
.toString());
}
if (isPubClient(req)) {
metaStore.increaseDownloads(name, version);
}
if (packageStore.supportsDownloadUrl) {
return shelf.Response.found(
await packageStore.downloadUrl(name, version));
} else {
return shelf.Response.ok(
packageStore.download(name, version),
headers: {HttpHeaders.contentTypeHeader: ContentType.binary.mimeType},
);
}
}
@Route.get('/api/packages/versions/new')
Future<shelf.Response> getUploadUrl(shelf.Request req) async {
return _okWithJson({
'url': _resolveUrl(req, '/api/packages/versions/newUpload')
.toString(),
'fields': {},
});
}
@Route.post('/api/packages/versions/newUpload')
Future<shelf.Response> upload(shelf.Request req) async {
try {
var uploader = await _getUploaderEmail(req);
var contentType = req.headers['content-type'];
if (contentType == null) throw 'invalid content type';
var mediaType = MediaType.parse(contentType);
var boundary = mediaType.parameters['boundary'];
if (boundary == null) throw 'invalid boundary';
var transformer = MimeMultipartTransformer(boundary);
MimeMultipart? fileData;
// The map below makes the runtime type checker happy.
// https://github.com/dart-lang/pub-dev/blob/19033f8154ca1f597ef5495acbc84a2bb368f16d/app/lib/fake/server/fake_storage_server.dart#L74
final stream = req.read().map((a) => a).transform(transformer);
await for (var part in stream) {
if (fileData != null) continue;
fileData = part;
}
var bb = await fileData!.fold(
BytesBuilder(), (BytesBuilder byteBuilder, d) => byteBuilder..add(d));
var tarballBytes = bb.takeBytes();
var tarBytes = GZipDecoder().decodeBytes(tarballBytes);
var archive = TarDecoder().decodeBytes(tarBytes);
ArchiveFile? pubspecArchiveFile;
ArchiveFile? readmeFile;
ArchiveFile? changelogFile;
for (var file in archive.files) {
if (file.name == 'pubspec.yaml') {
pubspecArchiveFile = file;
continue;
}
if (file.name.toLowerCase() == 'readme.md') {
readmeFile = file;
continue;
}
if (file.name.toLowerCase() == 'changelog.md') {
changelogFile = file;
continue;
}
}
if (pubspecArchiveFile == null) {
throw 'Did not find any pubspec.yaml file in upload. Aborting.';
}
var pubspecYaml = utf8.decode(pubspecArchiveFile.content);
var pubspec = loadYamlAsMap(pubspecYaml)!;
if (uploadValidator != null) {
await uploadValidator!(pubspec, uploader);
}
// TODO: null
var name = pubspec['name'] as String;
var version = pubspec['version'] as String;
var package = await metaStore.queryPackage(name);
// Package already exists
if (package != null) {
if (package.private == false) {
throw '$name is not a private package. Please upload it to https://pub.dev';
}
// Check uploaders
if (package.uploaders?.contains(uploader) == false) {
throw '$uploader is not an uploader of $name';
}
// Check duplicated version
var duplicated = package.versions
.firstWhereOrNull((item) => version == item.version);
if (duplicated != null) {
throw 'version invalid: $name@$version already exists.';
}
}
// Upload package tarball to storage
await packageStore.upload(name, version, tarballBytes);
String? readme;
String? changelog;
if (readmeFile != null) {
readme = utf8.decode(readmeFile.content);
}
if (changelogFile != null) {
changelog = utf8.decode(changelogFile.content);
}
// Write package meta to database
var unpubVersion = UnpubVersion(
version,
pubspec,
pubspecYaml,
uploader,
readme,
changelog,
DateTime.now(),
);
await metaStore.addVersion(name, unpubVersion);
// TODO: Upload docs
return shelf.Response.found(_resolveUrl(req, '/api/packages/versions/newUploadFinish'));
} catch (err) {
return shelf.Response.found(_resolveUrl(req, '/api/packages/versions/newUploadFinish?error=$err'));
}
}
@Route.get('/api/packages/versions/newUploadFinish')
Future<shelf.Response> uploadFinish(shelf.Request req) async {
var error = req.requestedUri.queryParameters['error'];
if (error != null) {
return _badRequest(error);
}
return _successMessage('Successfully uploaded package.');
}
@Route.post('/api/packages/<name>/uploaders')
Future<shelf.Response> addUploader(shelf.Request req, String name) async {
var body = await req.readAsString();
var email = Uri.splitQueryString(body)['email']!; // TODO: null
var operatorEmail = await _getUploaderEmail(req);
var package = await metaStore.queryPackage(name);
if (package?.uploaders?.contains(operatorEmail) == false) {
return _badRequest('no permission', status: HttpStatus.forbidden);
}
if (package?.uploaders?.contains(email) == true) {
return _badRequest('email already exists');
}
await metaStore.addUploader(name, email);
return _successMessage('uploader added');
}
@Route.delete('/api/packages/<name>/uploaders/<email>')
Future<shelf.Response> removeUploader(
shelf.Request req, String name, String email) async {
email = Uri.decodeComponent(email);
var operatorEmail = await _getUploaderEmail(req);
var package = await metaStore.queryPackage(name);
// TODO: null
if (package?.uploaders?.contains(operatorEmail) == false) {
return _badRequest('no permission', status: HttpStatus.forbidden);
}
if (package?.uploaders?.contains(email) == false) {
return _badRequest('email not uploader');
}
await metaStore.removeUploader(name, email);
return _successMessage('uploader removed');
}
@Route.get('/webapi/packages')
Future<shelf.Response> getPackages(shelf.Request req) async {
var params = req.requestedUri.queryParameters;
var size = int.tryParse(params['size'] ?? '') ?? 10;
var page = int.tryParse(params['page'] ?? '') ?? 0;
var sort = params['sort'] ?? 'download';
var q = params['q'];
String? keyword;
String? uploader;
String? dependency;
if (q == null) {
} else if (q.startsWith('email:')) {
uploader = q.substring(6).trim();
} else if (q.startsWith('dependency:')) {
dependency = q.substring(11).trim();
} else {
keyword = q;
}
final result = await metaStore.queryPackages(
size: size,
page: page,
sort: sort,
keyword: keyword,
uploader: uploader,
dependency: dependency,
);
var data = ListApi(result.count, [
for (var package in result.packages)
ListApiPackage(
package.name,
package.versions.last.pubspec['description'] as String?,
getPackageTags(package.versions.last.pubspec),
package.versions.last.version,
package.updatedAt,
)
]);
return _okWithJson({'data': data.toJson()});
}
@Route.get('/packages/<name>.json')
Future<shelf.Response> getPackageVersions(
shelf.Request req, String name) async {
var package = await metaStore.queryPackage(name);
if (package == null) {
return _badRequest('package not exists', status: HttpStatus.notFound);
}
var versions = package.versions.map((v) => v.version).toList();
versions.sort((a, b) {
return semver.Version.prioritize(
semver.Version.parse(b), semver.Version.parse(a));
});
return _okWithJson({
'name': name,
'versions': versions,
});
}
@Route.get('/webapi/package/<name>/<version>')
Future<shelf.Response> getPackageDetail(
shelf.Request req, String name, String version) async {
var package = await metaStore.queryPackage(name);
if (package == null) {
return _okWithJson({'error': 'package not exists'});
}
UnpubVersion? packageVersion;
if (version == 'latest') {
packageVersion = package.versions.last;
} else {
packageVersion =
package.versions.firstWhereOrNull((item) => item.version == version);
}
if (packageVersion == null) {
return _okWithJson({'error': 'version not exists'});
}
var versions = package.versions
.map((v) => DetailViewVersion(v.version, v.createdAt))
.toList();
versions.sort((a, b) {
return semver.Version.prioritize(
semver.Version.parse(b.version), semver.Version.parse(a.version));
});
var pubspec = packageVersion.pubspec;
List<String?> authors;
if (pubspec['author'] != null) {
authors = RegExp(r'<(.*?)>')
.allMatches(pubspec['author'])
.map((match) => match.group(1))
.toList();
} else if (pubspec['authors'] != null) {
authors = (pubspec['authors'] as List)
.map((author) => RegExp(r'<(.*?)>').firstMatch(author)!.group(1))
.toList();
} else {
authors = [];
}
var depMap = (pubspec['dependencies'] as Map? ?? {}).cast<String, String>();
var data = WebapiDetailView(
package.name,
packageVersion.version,
packageVersion.pubspec['description'] ?? '',
packageVersion.pubspec['homepage'] ?? '',
package.uploaders ?? [],
packageVersion.createdAt,
packageVersion.readme,
packageVersion.changelog,
versions,
authors,
depMap.keys.toList(),
getPackageTags(packageVersion.pubspec),
);
return _okWithJson({'data': data.toJson()});
}
@Route.get('/')
@Route.get('/packages')
@Route.get('/packages/<name>')
@Route.get('/packages/<name>/versions/<version>')
Future<shelf.Response> indexHtml(shelf.Request req) async {
return shelf.Response.ok(index_html.content,
headers: {HttpHeaders.contentTypeHeader: ContentType.html.mimeType});
}
@Route.get('/main.dart.js')
Future<shelf.Response> mainDartJs(shelf.Request req) async {
return shelf.Response.ok(main_dart_js.content,
headers: {HttpHeaders.contentTypeHeader: 'text/javascript'});
}
String _getBadgeUrl(String label, String message, String color,
Map<String, String> queryParameters) {
var badgeUri = Uri.parse('https://img.shields.io/static/v1');
return Uri(
scheme: badgeUri.scheme,
host: badgeUri.host,
path: badgeUri.path,
queryParameters: {
'label': label,
'message': message,
'color': color,
...queryParameters,
}).toString();
}
@Route.get('/badge/<type>/<name>')
Future<shelf.Response> badge(
shelf.Request req, String type, String name) async {
var queryParameters = req.requestedUri.queryParameters;
var package = await metaStore.queryPackage(name);
if (package == null) {
return shelf.Response.notFound('Not found');
}
switch (type) {
case 'v':
var latest = semver.Version.primary(package.versions
.map((pv) => semver.Version.parse(pv.version))
.toList());
var color = latest.major == 0 ? 'orange' : 'blue';
return shelf.Response.found(
_getBadgeUrl('unpub', latest.toString(), color, queryParameters));
case 'd':
return shelf.Response.found(_getBadgeUrl(
'downloads', package.download.toString(), 'blue', queryParameters));
default:
return shelf.Response.notFound('Not found');
}
}
}

View File

@ -0,0 +1,34 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app.dart';
// **************************************************************************
// ShelfRouterGenerator
// **************************************************************************
Router _$AppRouter(App service) {
final router = Router();
router.add('GET', r'/api/packages/<name>', service.getVersions);
router.add(
'GET', r'/api/packages/<name>/versions/<version>', service.getVersion);
router.add(
'GET', r'/packages/<name>/versions/<version>.tar.gz', service.download);
router.add('GET', r'/api/packages/versions/new', service.getUploadUrl);
router.add('POST', r'/api/packages/versions/newUpload', service.upload);
router.add(
'GET', r'/api/packages/versions/newUploadFinish', service.uploadFinish);
router.add('POST', r'/api/packages/<name>/uploaders', service.addUploader);
router.add('DELETE', r'/api/packages/<name>/uploaders/<email>',
service.removeUploader);
router.add('GET', r'/webapi/packages', service.getPackages);
router.add('GET', r'/packages/<name>.json', service.getPackageVersions);
router.add(
'GET', r'/webapi/package/<name>/<version>', service.getPackageDetail);
router.add('GET', r'/', service.indexHtml);
router.add('GET', r'/packages', service.indexHtml);
router.add('GET', r'/packages/<name>', service.indexHtml);
router.add('GET', r'/packages/<name>/versions/<version>', service.indexHtml);
router.add('GET', r'/main.dart.js', service.mainDartJs);
router.add('GET', r'/badge/<type>/<name>', service.badge);
return router;
}

View File

@ -0,0 +1,28 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package_store.dart';
class FileStore extends PackageStore {
String baseDir;
String Function(String name, String version)? getFilePath;
FileStore(this.baseDir, {this.getFilePath});
File _getTarballFile(String name, String version) {
final filePath =
getFilePath?.call(name, version) ?? '$name-$version.tar.gz';
return File(path.join(baseDir, filePath));
}
@override
Future<void> upload(String name, String version, List<int> content) async {
var file = _getTarballFile(name, version);
await file.create(recursive: true);
await file.writeAsBytes(content);
}
@override
Stream<List<int>> download(String name, String version) {
return _getTarballFile(name, version).openRead();
}
}

View File

@ -0,0 +1,22 @@
import 'package:unpub/src/models.dart';
abstract class MetaStore {
Future<UnpubPackage?> queryPackage(String name);
Future<void> addVersion(String name, UnpubVersion version);
Future<void> addUploader(String name, String email);
Future<void> removeUploader(String name, String email);
void increaseDownloads(String name, String version);
Future<UnpubQueryResult> queryPackages({
required int size,
required int page,
required String sort,
String? keyword,
String? uploader,
String? dependency,
});
}

View File

@ -0,0 +1,73 @@
import 'package:json_annotation/json_annotation.dart';
part 'models.g.dart';
DateTime identity(DateTime x) => x;
@JsonSerializable(includeIfNull: false)
class UnpubVersion {
final String version;
final Map<String, dynamic> pubspec;
final String? pubspecYaml;
final String? uploader; // TODO: not sure why null. keep it nullable
final String? readme;
final String? changelog;
@JsonKey(fromJson: identity, toJson: identity)
final DateTime createdAt;
UnpubVersion(
this.version,
this.pubspec,
this.pubspecYaml,
this.uploader,
this.readme,
this.changelog,
this.createdAt,
);
factory UnpubVersion.fromJson(Map<String, dynamic> map) =>
_$UnpubVersionFromJson(map);
Map<String, dynamic> toJson() => _$UnpubVersionToJson(this);
}
@JsonSerializable()
class UnpubPackage {
final String name;
final List<UnpubVersion> versions;
final bool private;
final List<String>? uploaders;
@JsonKey(fromJson: identity, toJson: identity)
final DateTime createdAt;
@JsonKey(fromJson: identity, toJson: identity)
final DateTime updatedAt;
final int? download;
UnpubPackage(
this.name,
this.versions,
this.private,
this.uploaders,
this.createdAt,
this.updatedAt,
this.download,
);
factory UnpubPackage.fromJson(Map<String, dynamic> map) =>
_$UnpubPackageFromJson(map);
}
@JsonSerializable()
class UnpubQueryResult {
int count;
List<UnpubPackage> packages;
UnpubQueryResult(this.count, this.packages);
factory UnpubQueryResult.fromJson(Map<String, dynamic> map) =>
_$UnpubQueryResultFromJson(map);
}

View File

@ -0,0 +1,74 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'models.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UnpubVersion _$UnpubVersionFromJson(Map<String, dynamic> json) => UnpubVersion(
json['version'] as String,
json['pubspec'] as Map<String, dynamic>,
json['pubspecYaml'] as String?,
json['uploader'] as String?,
json['readme'] as String?,
json['changelog'] as String?,
identity(json['createdAt'] as DateTime),
);
Map<String, dynamic> _$UnpubVersionToJson(UnpubVersion instance) {
final val = <String, dynamic>{
'version': instance.version,
'pubspec': instance.pubspec,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('pubspecYaml', instance.pubspecYaml);
writeNotNull('uploader', instance.uploader);
writeNotNull('readme', instance.readme);
writeNotNull('changelog', instance.changelog);
writeNotNull('createdAt', identity(instance.createdAt));
return val;
}
UnpubPackage _$UnpubPackageFromJson(Map<String, dynamic> json) => UnpubPackage(
json['name'] as String,
(json['versions'] as List<dynamic>)
.map((e) => UnpubVersion.fromJson(e as Map<String, dynamic>))
.toList(),
json['private'] as bool,
(json['uploaders'] as List<dynamic>?)?.map((e) => e as String).toList(),
identity(json['createdAt'] as DateTime),
identity(json['updatedAt'] as DateTime),
json['download'] as int?,
);
Map<String, dynamic> _$UnpubPackageToJson(UnpubPackage instance) =>
<String, dynamic>{
'name': instance.name,
'versions': instance.versions,
'private': instance.private,
'uploaders': instance.uploaders,
'createdAt': identity(instance.createdAt),
'updatedAt': identity(instance.updatedAt),
'download': instance.download,
};
UnpubQueryResult _$UnpubQueryResultFromJson(Map<String, dynamic> json) =>
UnpubQueryResult(
json['count'] as int,
(json['packages'] as List<dynamic>)
.map((e) => UnpubPackage.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$UnpubQueryResultToJson(UnpubQueryResult instance) =>
<String, dynamic>{
'count': instance.count,
'packages': instance.packages,
};

View File

@ -0,0 +1,104 @@
import 'package:mongo_dart/mongo_dart.dart';
import 'package:intl/intl.dart';
import 'package:unpub/src/models.dart';
import 'meta_store.dart';
final packageCollection = 'packages';
final statsCollection = 'stats';
class MongoStore extends MetaStore {
Db db;
MongoStore(this.db);
static SelectorBuilder _selectByName(String? name) => where.eq('name', name);
Future<UnpubQueryResult> _queryPackagesBySelector(
SelectorBuilder selector) async {
final count = await db.collection(packageCollection).count(selector);
final packages = await db
.collection(packageCollection)
.find(selector)
.map((item) => UnpubPackage.fromJson(item))
.toList();
return UnpubQueryResult(count, packages);
}
@override
queryPackage(name) async {
var json =
await db.collection(packageCollection).findOne(_selectByName(name));
if (json == null) return null;
return UnpubPackage.fromJson(json);
}
@override
addVersion(name, version) async {
await db.collection(packageCollection).update(
_selectByName(name),
modify
.push('versions', version.toJson())
.addToSet('uploaders', version.uploader)
.setOnInsert('createdAt', version.createdAt)
.setOnInsert('private', true)
.setOnInsert('download', 0)
.set('updatedAt', version.createdAt),
upsert: true);
}
@override
addUploader(name, email) async {
await db
.collection(packageCollection)
.update(_selectByName(name), modify.push('uploaders', email));
}
@override
removeUploader(name, email) async {
await db
.collection(packageCollection)
.update(_selectByName(name), modify.pull('uploaders', email));
}
@override
increaseDownloads(name, version) {
var today = DateFormat('yyyyMMdd').format(DateTime.now());
db
.collection(packageCollection)
.update(_selectByName(name), modify.inc('download', 1));
db
.collection(statsCollection)
.update(_selectByName(name), modify.inc('d$today', 1));
}
@override
Future<UnpubQueryResult> queryPackages({
required size,
required page,
required sort,
keyword,
uploader,
dependency,
}) {
var selector =
where.sortBy(sort, descending: true).limit(size).skip(page * size);
if (keyword != null) {
selector = selector.match('name', '.*$keyword.*');
}
if (uploader != null) {
selector = selector.eq('uploaders', uploader);
}
if (dependency != null) {
selector = selector.raw({
'versions': {
r'$elemMatch': {
'pubspec.dependencies.$dependency': {r'$exists': true}
}
}
});
}
return _queryPackagesBySelector(selector);
}
}

View File

@ -0,0 +1,15 @@
import 'dart:async';
abstract class PackageStore {
bool supportsDownloadUrl = false;
FutureOr<String> downloadUrl(String name, String version) {
throw 'downloadUri not implemented';
}
Stream<List<int>> download(String name, String version) {
throw 'download not implemented';
}
Future<void> upload(String name, String version, List<int> content);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,27 @@
import 'package:yaml/yaml.dart';
convertYaml(dynamic value) {
if (value is YamlMap) {
return value
.cast<String, dynamic>()
.map((k, v) => MapEntry(k, convertYaml(v)));
}
if (value is YamlList) {
return value.map((e) => convertYaml(e)).toList();
}
return value;
}
Map<String, dynamic>? loadYamlAsMap(dynamic value) {
var yamlMap = loadYaml(value) as YamlMap?;
return convertYaml(yamlMap).cast<String, dynamic>();
}
List<String> getPackageTags(Map<String, dynamic> pubspec) {
// TODO: web and other tags
if (pubspec['flutter'] != null) {
return ['flutter'];
} else {
return ['flutter', 'web', 'other'];
}
}

View File

@ -0,0 +1,6 @@
export 'src/meta_store.dart';
export 'src/mongo_store.dart';
export 'src/package_store.dart';
export 'src/file_store.dart';
export 'src/app.dart';
export 'src/models.dart';

View File

@ -0,0 +1,78 @@
import 'package:json_annotation/json_annotation.dart';
part 'models.g.dart';
@JsonSerializable()
class ListApi {
int count;
List<ListApiPackage> packages;
ListApi(this.count, this.packages);
factory ListApi.fromJson(Map<String, dynamic> map) => _$ListApiFromJson(map);
Map<String, dynamic> toJson() => _$ListApiToJson(this);
}
@JsonSerializable()
class ListApiPackage {
String name;
String? description;
List<String> tags;
String latest;
DateTime updatedAt;
ListApiPackage(
this.name, this.description, this.tags, this.latest, this.updatedAt);
factory ListApiPackage.fromJson(Map<String, dynamic> map) =>
_$ListApiPackageFromJson(map);
Map<String, dynamic> toJson() => _$ListApiPackageToJson(this);
}
@JsonSerializable()
class DetailViewVersion {
String version;
DateTime createdAt;
DetailViewVersion(this.version, this.createdAt);
factory DetailViewVersion.fromJson(Map<String, dynamic> map) =>
_$DetailViewVersionFromJson(map);
Map<String, dynamic> toJson() => _$DetailViewVersionToJson(this);
}
@JsonSerializable()
class WebapiDetailView {
String name;
String version;
String description;
String homepage;
List<String> uploaders;
DateTime createdAt;
final String? readme;
final String? changelog;
List<DetailViewVersion> versions;
List<String?> authors;
List<String>? dependencies;
List<String> tags;
WebapiDetailView(
this.name,
this.version,
this.description,
this.homepage,
this.uploaders,
this.createdAt,
this.readme,
this.changelog,
this.versions,
this.authors,
this.dependencies,
this.tags);
factory WebapiDetailView.fromJson(Map<String, dynamic> map) =>
_$WebapiDetailViewFromJson(map);
Map<String, dynamic> toJson() => _$WebapiDetailViewToJson(this);
}

View File

@ -0,0 +1,85 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'models.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ListApi _$ListApiFromJson(Map<String, dynamic> json) => ListApi(
json['count'] as int,
(json['packages'] as List<dynamic>)
.map((e) => ListApiPackage.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ListApiToJson(ListApi instance) => <String, dynamic>{
'count': instance.count,
'packages': instance.packages,
};
ListApiPackage _$ListApiPackageFromJson(Map<String, dynamic> json) =>
ListApiPackage(
json['name'] as String,
json['description'] as String?,
(json['tags'] as List<dynamic>).map((e) => e as String).toList(),
json['latest'] as String,
DateTime.parse(json['updatedAt'] as String),
);
Map<String, dynamic> _$ListApiPackageToJson(ListApiPackage instance) =>
<String, dynamic>{
'name': instance.name,
'description': instance.description,
'tags': instance.tags,
'latest': instance.latest,
'updatedAt': instance.updatedAt.toIso8601String(),
};
DetailViewVersion _$DetailViewVersionFromJson(Map<String, dynamic> json) =>
DetailViewVersion(
json['version'] as String,
DateTime.parse(json['createdAt'] as String),
);
Map<String, dynamic> _$DetailViewVersionToJson(DetailViewVersion instance) =>
<String, dynamic>{
'version': instance.version,
'createdAt': instance.createdAt.toIso8601String(),
};
WebapiDetailView _$WebapiDetailViewFromJson(Map<String, dynamic> json) =>
WebapiDetailView(
json['name'] as String,
json['version'] as String,
json['description'] as String,
json['homepage'] as String,
(json['uploaders'] as List<dynamic>).map((e) => e as String).toList(),
DateTime.parse(json['createdAt'] as String),
json['readme'] as String?,
json['changelog'] as String?,
(json['versions'] as List<dynamic>)
.map((e) => DetailViewVersion.fromJson(e as Map<String, dynamic>))
.toList(),
(json['authors'] as List<dynamic>).map((e) => e as String?).toList(),
(json['dependencies'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
(json['tags'] as List<dynamic>).map((e) => e as String).toList(),
);
Map<String, dynamic> _$WebapiDetailViewToJson(WebapiDetailView instance) =>
<String, dynamic>{
'name': instance.name,
'version': instance.version,
'description': instance.description,
'homepage': instance.homepage,
'uploaders': instance.uploaders,
'createdAt': instance.createdAt.toIso8601String(),
'readme': instance.readme,
'changelog': instance.changelog,
'versions': instance.versions,
'authors': instance.authors,
'dependencies': instance.dependencies,
'tags': instance.tags,
};

View File

@ -0,0 +1,4 @@
name: unpub_api
environment:
sdk: ">=2.12.0 <3.0.0"

View File

@ -0,0 +1,33 @@
name: unpub
description: Self-hosted private Dart Pub server for Enterprise, with a simple web interface to search and view packages information.
version: 2.1.0
homepage: https://github.com/bytedance/unpub
environment:
sdk: ">=2.12.0 <3.0.0"
executables:
unpub: unpub
dependencies:
args: ^2.2.0
shelf: ^1.2.0
shelf_router: ^1.1.1
http_parser: ^4.0.0
http: ^0.13.3
archive: ^3.1.2
logging: ^1.0.1
meta: ^1.1.7
googleapis: ^4.0.0
yaml: ^3.1.0
pub_semver: ^2.0.0
json_annotation: ^4.1.0
mongo_dart: ^0.7.1
mime: ^1.0.0
intl: ^0.17.0
path: ^1.6.2
collection: ^1.15.0
shelf_cors_headers: ^0.1.2
dev_dependencies:
test: ^1.6.1
build_runner: ^2.1.1
json_serializable: ^5.0.0
shelf_router_generator: ^1.0.1
chunked_stream: ^1.4.1

View File

@ -0,0 +1,57 @@
import 'dart:io';
import 'package:chunked_stream/chunked_stream.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:unpub/unpub.dart' as unpub;
//test gzip data
const TEST_PKG_DATA = [
0x8b, 0x1f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, //
0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 //
];
main() {
test('upload-download-default-path', () async {
var baseDir = _setup_fixture('upload-download-default-path');
var store = unpub.FileStore(baseDir.path);
await store.upload('test_package', '1.0.0', TEST_PKG_DATA);
var pkg2 = await readByteStream(store.download('test_package', '1.0.0'));
expect(pkg2, TEST_PKG_DATA);
expect(
File(path.join(baseDir.path, 'test_package-1.0.0.tar.gz')).existsSync(),
isTrue);
});
test('upload-download-custom-path', () async {
var baseDir = _setup_fixture('upload-download-custom-path');
var store = unpub.FileStore(baseDir.path, getFilePath: newFilePathFunc());
await store.upload('test_package', '1.0.0', TEST_PKG_DATA);
var pkg2 = await readByteStream(store.download('test_package', '1.0.0'));
expect(pkg2, TEST_PKG_DATA);
expect(
File(path.join(baseDir.path, 'packages', 't', 'te', 'test_package',
'versions', 'test_package-1.0.0.tar.gz'))
.existsSync(),
isTrue);
});
}
String Function(String, String) newFilePathFunc() {
return (String package, String version) {
var grp = package[0];
var subgrp = package.substring(0, 2);
return path.join('packages', grp, subgrp, package, 'versions',
'$package-$version.tar.gz');
};
}
_setup_fixture(final String name) {
var baseDir =
Directory(path.absolute('test', 'fixtures', 'file_store', name));
if (baseDir.existsSync()) {
baseDir.deleteSync(recursive: true);
}
baseDir.createSync();
return baseDir;
}

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1 @@
# 0.0.1

View File

@ -0,0 +1 @@
# package0 0.0.1

View File

@ -0,0 +1,6 @@
name: package_0
description: test
version: 0.0.1
homepage: https://example.com
environment:
sdk: ">=2.0.0 <3.0.0"

View File

@ -0,0 +1,3 @@
# 0.0.2
# 0.0.1

View File

@ -0,0 +1 @@
# package0 0.0.2

View File

@ -0,0 +1,6 @@
name: package_0
description: test
version: 0.0.2
homepage: https://example.com
environment:
sdk: ">=2.0.0 <3.0.0"

View File

@ -0,0 +1,7 @@
# 0.0.3+1
# 0.0.3
# 0.0.2
# 0.0.1

View File

@ -0,0 +1 @@
# package0 0.0.3+1

View File

@ -0,0 +1,6 @@
name: package_0
description: test
version: 0.0.3+1
homepage: https://example.com
environment:
sdk: ">=2.0.0 <3.0.0"

View File

@ -0,0 +1,5 @@
# 0.0.3
# 0.0.2
# 0.0.1

View File

@ -0,0 +1 @@
# package0 0.0.3

View File

@ -0,0 +1,6 @@
name: package_0
description: test
version: 0.0.3
homepage: https://example.com
environment:
sdk: ">=2.0.0 <3.0.0"

View File

@ -0,0 +1,6 @@
name: package_0
description: test
version: 1.0.0-noreadme
homepage: https://example.com
environment:
sdk: ">=2.0.0 <3.0.0"

View File

@ -0,0 +1,6 @@
name: package_0
description: test
version: 1.0.0
homepage: https://example.com
environment:
sdk: ">=2.0.0 <3.0.0"

View File

@ -0,0 +1,6 @@
name: package_1
description: test
version: 0.0.1
homepage: https://example.com
environment:
sdk: ">=2.0.0 <3.0.0"

View File

@ -0,0 +1,407 @@
import 'dart:io';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:unpub/src/utils.dart';
import 'package:test/test.dart';
import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http;
import 'package:mongo_dart/mongo_dart.dart';
import 'utils.dart';
import 'package:unpub/unpub.dart';
main() {
Db _db = Db('mongodb://localhost:27017/dart_pub_test');
late HttpServer _server;
setUpAll(() async {
await _db.open();
});
Future<Map<String, dynamic>> _readMeta(String name) async {
var res =
await _db.collection(packageCollection).findOne(where.eq('name', name));
res!.remove('_id'); // TODO: null
return res;
}
Map<String, String> _pubspecCache = {};
Future<String?> _readFile(
String package, String version, String filename) async {
var key = package + version + filename;
if (_pubspecCache[key] == null) {
var filePath = path.absolute('test/fixtures', package, version, filename);
_pubspecCache[key] = await File(filePath).readAsString();
}
return _pubspecCache[key];
}
_cleanUpDb() async {
await _db.dropCollection(packageCollection);
await _db.dropCollection(statsCollection);
}
tearDownAll(() async {
await _db.close();
});
group('publish', () {
setUpAll(() async {
await _cleanUpDb();
_server = await createServer(email0);
});
tearDownAll(() async {
await _server.close();
});
test('fresh', () async {
var version = '0.0.1';
var result = await pubPublish(package0, version);
expect(result.stderr, '');
var meta = await _readMeta(package0);
expect(meta['name'], package0);
expect(meta['uploaders'], [email0]);
expect(meta['private'], true);
expect(meta['createdAt'], isA<DateTime>());
expect(meta['updatedAt'], isA<DateTime>());
expect(meta['versions'], isList);
expect(meta['versions'], hasLength(1));
var item = meta['versions'][0];
expect(item['createdAt'], isA<DateTime>());
item.remove('createdAt');
expect(
DeepCollectionEquality().equals(item, {
'version': version,
'pubspecYaml': await _readFile(package0, version, 'pubspec.yaml'),
'pubspec':
loadYamlAsMap(await _readFile(package0, version, 'pubspec.yaml')),
'readme': await _readFile(package0, version, 'README.md'),
'changelog': await _readFile(package0, version, 'CHANGELOG.md'),
'uploader': email0,
}),
true,
);
});
test('existing package', () async {
var version = '0.0.3';
var result = await pubPublish(package0, version);
expect(result.stderr, '');
var meta = await _readMeta(package0);
expect(meta['name'], package0);
expect(meta['uploaders'], [email0]);
expect(meta['versions'], isList);
expect(meta['versions'], hasLength(2));
expect(meta['versions'][0]['version'], '0.0.1');
expect(meta['versions'][1]['version'], version);
});
test('duplicated version', () async {
var result = await pubPublish(package0, '0.0.3');
expect(result.stderr, contains('version invalid'));
});
test('no readme and changelog', () async {
var version = '1.0.0-noreadme';
var result = await pubPublish(package0, version);
// expect(result.stderr, ''); // Suggestions:
var meta = await _readMeta(package0);
expect(meta['name'], package0);
expect(meta['uploaders'], [email0]);
expect(meta['versions'], isList);
expect(meta['versions'], hasLength(3));
expect(meta['versions'][0]['version'], '0.0.1');
expect(meta['versions'][1]['version'], '0.0.3');
var item = meta['versions'][2];
expect(item['createdAt'], isA<DateTime>());
item.remove('createdAt');
expect(
DeepCollectionEquality().equals(item, {
'version': version,
'pubspecYaml': await _readFile(package0, version, 'pubspec.yaml'),
'pubspec':
loadYamlAsMap(await _readFile(package0, version, 'pubspec.yaml')),
'uploader': email0,
}),
true,
);
});
});
group('get versions', () {
setUpAll(() async {
await _cleanUpDb();
_server = await createServer(email0);
await pubPublish(package0, '0.0.1');
await pubPublish(package0, '0.0.2');
});
tearDownAll(() async {
await _server.close();
});
test('existing at local', () async {
var res = await getVersions(package0);
expect(res.statusCode, HttpStatus.ok);
var body = json.decode(res.body);
expect(
DeepCollectionEquality().equals(body, {
"name": "package_0",
"latest": {
"archive_url":
"$pubHostedUrl/packages/package_0/versions/0.0.2.tar.gz",
"pubspec": loadYamlAsMap(
await _readFile('package_0', '0.0.2', 'pubspec.yaml')),
"version": "0.0.2"
},
"versions": [
{
"archive_url":
"$pubHostedUrl/packages/package_0/versions/0.0.1.tar.gz",
"pubspec": loadYamlAsMap(
await _readFile('package_0', '0.0.1', 'pubspec.yaml')),
"version": "0.0.1"
},
{
"archive_url":
"$pubHostedUrl/packages/package_0/versions/0.0.2.tar.gz",
"pubspec": loadYamlAsMap(
await _readFile('package_0', '0.0.2', 'pubspec.yaml')),
"version": "0.0.2"
}
]
}),
true,
);
});
test('existing at remote', () async {
var name = 'http';
var res = await getVersions(name);
expect(res.statusCode, HttpStatus.ok);
var body = json.decode(res.body);
expect(body['name'], name);
});
test('not existing', () async {
var res = await getVersions(notExistingPacakge);
expect(res.statusCode, HttpStatus.notFound);
});
});
group('get specific version', () {
setUpAll(() async {
await _cleanUpDb();
_server = await createServer(email0);
await pubPublish(package0, '0.0.1');
await pubPublish(package0, '0.0.3+1');
});
tearDownAll(() async {
await _server.close();
});
test('existing at local', () async {
var res = await getSpecificVersion(package0, '0.0.1');
expect(res.statusCode, HttpStatus.ok);
var body = json.decode(res.body);
expect(
DeepCollectionEquality().equals(body, {
"archive_url":
"$pubHostedUrl/packages/package_0/versions/0.0.1.tar.gz",
"pubspec": loadYamlAsMap(
await _readFile('package_0', '0.0.1', 'pubspec.yaml')),
"version": '0.0.1'
}),
true,
);
});
test('decode version correctly', () async {
var res = await getSpecificVersion(package0, '0.0.3+1');
expect(res.statusCode, HttpStatus.ok);
var body = json.decode(res.body);
expect(
DeepCollectionEquality().equals(body, {
"archive_url":
"$pubHostedUrl/packages/package_0/versions/0.0.3+1.tar.gz",
"pubspec": loadYamlAsMap(
await _readFile('package_0', '0.0.3+1', 'pubspec.yaml')),
"version": '0.0.3+1'
}),
true,
);
});
test('not existing version at local', () async {
var res = await getSpecificVersion(package0, '0.0.2');
expect(res.statusCode, HttpStatus.notFound);
});
test('existing at remote', () async {
var res = await getSpecificVersion('http', '0.12.0+2');
expect(res.statusCode, HttpStatus.ok);
var body = json.decode(res.body);
expect(body['version'], '0.12.0+2');
});
test('not existing', () async {
var res = await getSpecificVersion(notExistingPacakge, '0.0.1');
expect(res.statusCode, HttpStatus.notFound);
});
});
group('uploader', () {
setUpAll(() async {
await _cleanUpDb();
_server = await createServer(email0);
await pubPublish(package0, '0.0.1');
});
tearDownAll(() async {
await _server.close();
});
group('add', () {
test('already exists', () async {
var result = await pubUploader(package0, 'add', email0);
expect(result.stderr, contains('email already exists'));
var meta = await _readMeta(package0);
expect(meta['uploaders'], unorderedEquals([email0]));
});
test('success', () async {
var result = await pubUploader(package0, 'add', email1);
expect(result.stderr, '');
var meta = await _readMeta(package0);
expect(meta['uploaders'], unorderedEquals([email0, email1]));
result = await pubUploader(package0, 'add', email2);
expect(result.stderr, '');
meta = await _readMeta(package0);
expect(meta['uploaders'], unorderedEquals([email0, email1, email2]));
});
});
group('remove', () {
test('not in uploader', () async {
var result = await pubUploader(package0, 'remove', email3);
expect(result.stderr, contains('email not uploader'));
var meta = await _readMeta(package0);
expect(meta['uploaders'], unorderedEquals([email0, email1, email2]));
});
test('success', () async {
var result = await pubUploader(package0, 'remove', email2);
expect(result.stderr, '');
var meta = await _readMeta(package0);
expect(meta['uploaders'], unorderedEquals([email0, email1]));
result = await pubUploader(package0, 'remove', email1);
expect(result.stderr, '');
meta = await _readMeta(package0);
expect(meta['uploaders'], unorderedEquals([email0]));
});
});
group('permission', () {
setUpAll(() async {
await _server.close();
_server = await createServer(email1);
});
tearDownAll(() async {
await _server.close();
});
test('add', () async {
var result = await pubUploader(package0, 'add', email0);
expect(result.stderr, contains('no permission'));
});
test('remove', () async {
var result = await pubUploader(package0, 'remove', email0);
expect(result.stderr, contains('no permission'));
});
});
});
group('badge', () {
setUpAll(() async {
await _cleanUpDb();
_server = await createServer(email0);
await pubPublish(package0, '0.0.1');
});
tearDownAll(() async {
await _server.close();
});
group('v', () {
test('<1.0.0', () async {
var res = await http.Client().send(
http.Request('GET', baseUri.resolve('/badge/v/$package0'))
..followRedirects = false);
expect(res.statusCode, HttpStatus.found);
expect(res.headers[HttpHeaders.locationHeader],
'https://img.shields.io/static/v1?label=unpub&message=0.0.1&color=orange');
});
test('>=1.0.0', () async {
await pubPublish(package0, '1.0.0');
var res = await http.Client().send(
http.Request('GET', baseUri.resolve('/badge/v/$package0'))
..followRedirects = false);
expect(res.statusCode, HttpStatus.found);
expect(res.headers[HttpHeaders.locationHeader],
'https://img.shields.io/static/v1?label=unpub&message=1.0.0&color=blue');
});
test('package not exists', () async {
var res =
await http.get(baseUri.resolve('/badge/v/$notExistingPacakge'));
expect(res.statusCode, HttpStatus.notFound);
});
});
group('d', () {
test('correct download count', () async {
var res = await http.Client().send(
http.Request('GET', baseUri.resolve('/badge/d/$package0'))
..followRedirects = false);
expect(res.statusCode, HttpStatus.found);
expect(res.headers[HttpHeaders.locationHeader],
'https://img.shields.io/static/v1?label=downloads&message=0&color=blue');
});
test('package not exists', () async {
var res =
await http.get(baseUri.resolve('/badge/d/$notExistingPacakge'));
expect(res.statusCode, HttpStatus.notFound);
});
});
});
}

View File

@ -0,0 +1,57 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http;
import 'package:unpub/unpub.dart' as unpub;
import 'package:mongo_dart/mongo_dart.dart';
final notExistingPacakge = 'not_existing_package';
final baseDir = path.absolute('unpub-packages');
final pubHostedUrl = 'http://localhost:4000';
final baseUri = Uri.parse(pubHostedUrl);
final package0 = 'package_0';
final package1 = 'package_1';
final email0 = 'email0@example.com';
final email1 = 'email1@example.com';
final email2 = 'email2@example.com';
final email3 = 'email3@example.com';
createServer(String opEmail) async {
final db = Db('mongodb://localhost:27017/dart_pub_test');
await db.open();
var mongoStore = unpub.MongoStore(db);
var app = unpub.App(
metaStore: mongoStore,
packageStore: unpub.FileStore(baseDir),
overrideUploaderEmail: opEmail,
);
var server = await app.serve('0.0.0.0', 4000);
return server;
}
Future<http.Response> getVersions(String package) {
package = Uri.encodeComponent(package);
return http.get(baseUri.resolve('/api/packages/$package'));
}
Future<http.Response> getSpecificVersion(String package, String version) {
package = Uri.encodeComponent(package);
version = Uri.encodeComponent(version);
return http.get(baseUri.resolve('/api/packages/$package/versions/$version'));
}
Future<ProcessResult> pubPublish(String name, String version) {
return Process.run('dart', ['pub', 'publish', '--force'],
workingDirectory: path.absolute('test/fixtures', name, version),
environment: {'PUB_HOSTED_URL': pubHostedUrl});
}
Future<ProcessResult> pubUploader(String name, String operation, String email) {
assert(['add', 'remove'].contains(operation), 'operation error');
return Process.run('dart', ['pub', 'uploader', operation, email],
workingDirectory: path.absolute('test/fixtures', name, '0.0.1'),
environment: {'PUB_HOSTED_URL': pubHostedUrl});
}

View File

@ -0,0 +1,14 @@
import 'dart:io';
import 'package:path/path.dart' as path;
var files = ['index.html', 'main.dart.js'];
main(List<String> args) {
for (var file in files) {
var content =
File(path.absolute('unpub_web/build', file)).readAsStringSync();
content = content.replaceAll('\\', '\\\\').replaceAll('\$', '\\\$');
content = 'const content = """$content""";\n';
File(path.absolute('unpub/lib/src/static', '${file}.dart')).writeAsStringSync(content);
}
}

View File

@ -0,0 +1,9 @@
name: "unpub_server"
description: "Configures unpub accordingly"
dependencies:
unpub:
path: packages/unpub
environment:
sdk: ">=2.15.0 <3.0.0"