initial
This commit is contained in:
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Hidden Dart tool files
|
||||
**/.dart_tool/
|
||||
|
||||
# Exclude pubspec.lock (optional – see explanation below)
|
||||
**/pubspec.lock
|
||||
|
||||
# Build artifacts
|
||||
**/build/
|
||||
**/dart_tool/
|
||||
|
||||
# IDE/editor-related files (optional)
|
||||
*.iml
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
# Other temporary files
|
||||
*.log
|
||||
|
||||
# Volumes folder
|
||||
volumes/
|
||||
36
Dockerfile
Normal file
36
Dockerfile
Normal file
@ -0,0 +1,36 @@
|
||||
#
|
||||
# --- STAGE 1: Build ---
|
||||
#
|
||||
FROM dart:stable AS build
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copying all app recources
|
||||
COPY res/unpub-server .
|
||||
|
||||
# Install dependencies
|
||||
RUN dart pub get
|
||||
|
||||
# Create build directory
|
||||
RUN mkdir -p build
|
||||
|
||||
# Compiling the server
|
||||
RUN dart compile exe lib/server.dart -o build/server
|
||||
|
||||
#
|
||||
# --- STAGE 2: Runtime ---
|
||||
#
|
||||
FROM debian:bullseye-slim AS runtime
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Coping executable
|
||||
COPY --from=build /app/build/server .
|
||||
|
||||
# Expose http port, see docker-compose.yaml
|
||||
EXPOSE 8080
|
||||
|
||||
# Start server, when starting the container
|
||||
CMD ["./server"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Dennis Skupin
|
||||
|
||||
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.
|
||||
189
README.md
Normal file
189
README.md
Normal file
@ -0,0 +1,189 @@
|
||||
# Unpub
|
||||
|
||||
> 🧪 This is a small adaption of [unpub](https://pub.dev/packages/unpub) to docker.
|
||||
|
||||
---
|
||||
|
||||
## 📦 About
|
||||
|
||||
Unpub is a self-hosted private Dart Pub server, designed for enterprise use.
|
||||
It includes a simple web interface for browsing and searching package information.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ MongoDB Compatibility
|
||||
|
||||
> The latest version of Unpub is **v2.1.0**, which was released over 2 years ago.
|
||||
> Using MongoDB version 4.4 in conjunction provides the most stable experience.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Publishing a Package
|
||||
|
||||
Make sure your `pubspec.yaml` contains the following:
|
||||
|
||||
```yaml
|
||||
name: <package_name>
|
||||
description: <short description>
|
||||
version: <semantic_version>
|
||||
repository: <unpub_server_url>/packages/<package_name>
|
||||
publish_to: <unpub_server_url>
|
||||
```
|
||||
|
||||
### 1. Publish using Dart:
|
||||
|
||||
```bash
|
||||
dart pub publish
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Authentication issues?
|
||||
|
||||
If you encounter problems during publishing, set a fake auth token:
|
||||
|
||||
Install and use [`unpub_auth`](https://pub.dev/documentation/unpub_auth/latest/):
|
||||
|
||||
```bash
|
||||
dart pub global activate unpub_auth # install the auth tool
|
||||
unpub_auth login # log in via provided URL
|
||||
unpub_auth get | dart pub token add <unpub_server_url>
|
||||
# add token to pub client
|
||||
dart pub publish # then try again
|
||||
```
|
||||
|
||||
> The email address used for login will be displayed on the package’s webpage under the uploader section.
|
||||
|
||||
---
|
||||
|
||||
## 📥 Retrieving Packages
|
||||
|
||||
To use a package hosted on Unpub server, add the following to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
<package_name>:
|
||||
hosted:
|
||||
name: <package_name>
|
||||
url: <unpub_server_url>
|
||||
version: <semantic_version>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker Setup
|
||||
|
||||
This project includes a Docker-based deployment setup.
|
||||
|
||||
1. **Adjust** Unpub settings (especially the version) in `res/unpub/pubspec.yaml`
|
||||
2. **Build the image:**
|
||||
```bash
|
||||
docker image build -t unpub .
|
||||
```
|
||||
3. **Start containers:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
> 🔧 Make sure to adjust the `docker-compose.yaml` file according to your needs before running the containers.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Customizing the Web UI
|
||||
|
||||
To modify the web interface (e.g. logo, colors, layout):
|
||||
|
||||
- Edit the files in the `res/web` directory
|
||||
- **Do this _before_ building the Docker image**, so your changes are included
|
||||
|
||||
---
|
||||
|
||||
## 💽 Backup & Restore Data
|
||||
|
||||
Packages are stored in the volume `volumes/packages-data`, the corresponding metadata in MongoDB (volume `volumes/db-data`). Make sure the volumes (see `docker-compose.yaml`) are **not deleted**.
|
||||
|
||||
In terms of prevent data lost, the volume `volumes/packages-data` has only be copied, but
|
||||
just copying the volume `volumes/db-data` is not the best choice, additionally, you have the following options:
|
||||
|
||||
### 📦 Download a Package Archive
|
||||
|
||||
On the Unpub web interface, you can download an archive of any package version directly under the **Versions** section of the package page.
|
||||
|
||||
---
|
||||
|
||||
### 🔄 Create a MongoDB Backup
|
||||
|
||||
#### 1. Create a backup inside the container (e.g., under `/data/backup`)
|
||||
|
||||
```bash
|
||||
docker exec mongo-unpub mongodump --out=/data/backup
|
||||
```
|
||||
|
||||
#### 2. Copy the backup to the host machine
|
||||
|
||||
```bash
|
||||
docker cp mongo-unpub:/data/backup ./mongo-backup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ♻️ Restore a MongoDB Backup
|
||||
|
||||
#### 1. Copy the backup back into the container
|
||||
|
||||
```bash
|
||||
docker cp ./mongo-backup mongo-unpub:/data/restore
|
||||
```
|
||||
|
||||
#### 2. Start the restore process
|
||||
|
||||
```bash
|
||||
docker exec mongo-unpub mongorestore /data/restore
|
||||
```
|
||||
|
||||
🔁 Optional: Drop existing data before restoring:
|
||||
|
||||
```bash
|
||||
docker exec mongo-unpub mongorestore --drop /data/restore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🗄️ Using the Provided Backup Service
|
||||
|
||||
A dedicated backup service is included to simplify data preservation. It performs a full backup by copying all unpub packages and dumping the MongoDB database in one step.
|
||||
|
||||
To run the backup, use the following command:
|
||||
|
||||
```bash
|
||||
docker-compose run --rm backup
|
||||
```
|
||||
|
||||
The resulting archive file will be saved in the `volumes/backups` directory.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Access
|
||||
|
||||
Once deployed, you can access the web interface via the unpub server address:
|
||||
|
||||
```
|
||||
scheme://<unpub-server>/
|
||||
```
|
||||
|
||||
Depending on the settings in `docker-compose.yaml` the uri can differ.
|
||||
Without reverse proxy and other settings the server can be reached under the adress:
|
||||
|
||||
```
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 👤 Author
|
||||
|
||||
Dennis Skupin
|
||||
Based on the [unpub](https://pub.dev/packages/unpub) package by [Bytedance](https://pub.dev/packages/unpub)
|
||||
Licensed under the [MIT License](LICENSE)
|
||||
|
||||
---
|
||||
63
docker-compose.yaml
Executable file
63
docker-compose.yaml
Executable file
@ -0,0 +1,63 @@
|
||||
version: "3.8"
|
||||
|
||||
networks:
|
||||
unpub:
|
||||
name: unpub
|
||||
external: false
|
||||
|
||||
services:
|
||||
unpub:
|
||||
image: unpub:latest
|
||||
container_name: unpub
|
||||
depends_on:
|
||||
- mongo
|
||||
ports:
|
||||
- "4040:8080"
|
||||
volumes:
|
||||
- "./volumes/packages-data:/unpub-packages"
|
||||
networks:
|
||||
- unpub
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- UNPUB_PORT=8080
|
||||
- UNPUB_DB_URI=mongodb://mongo:27017/unpub
|
||||
# - UNPUB_REVERSE_PROXY_URI=
|
||||
restart: unless-stopped
|
||||
|
||||
mongo:
|
||||
image: mongo:4.4
|
||||
container_name: "mongo-unpub"
|
||||
volumes:
|
||||
- "./volumes/db-data:/data/db"
|
||||
networks:
|
||||
- unpub
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
restart: unless-stopped
|
||||
|
||||
backup:
|
||||
image: mongo:4.4
|
||||
container_name: "backup-unpub"
|
||||
depends_on:
|
||||
- unpub
|
||||
- mongo
|
||||
volumes:
|
||||
- ./volumes/packages-data:/unpub-packages:ro
|
||||
- ./volumes/backups:/backups
|
||||
networks:
|
||||
- unpub
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
command: >
|
||||
bash -c '
|
||||
B_DIR="/backups/backup_$$(date +%H%M%S_%d%m%Y)" &&
|
||||
mkdir -p "$$B_DIR" &&
|
||||
echo "📦 Backing up packages..." &&
|
||||
cp -rp /unpub-packages "$$B_DIR" &&
|
||||
echo "🧠 Dumping MongoDB..." &&
|
||||
mongodump --host=mongo --port=27017 --out="$$B_DIR/mongo-backup" &&
|
||||
tar czf "$${B_DIR}.tar.gz" -C "$$B_DIR" . &&
|
||||
rm -rf "$$B_DIR" &&
|
||||
echo "✅ Backup complete. Files in $${B_DIR}.tar.gz"
|
||||
'
|
||||
restart: "no"
|
||||
21
res/unpub-server/lib/server.dart
Normal file
21
res/unpub-server/lib/server.dart
Normal 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}');
|
||||
}
|
||||
60
res/unpub-server/packages/unpub/CHANGELOG.md
Normal file
60
res/unpub-server/packages/unpub/CHANGELOG.md
Normal 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
|
||||
21
res/unpub-server/packages/unpub/LICENSE
Normal file
21
res/unpub-server/packages/unpub/LICENSE
Normal 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.
|
||||
142
res/unpub-server/packages/unpub/README.md
Normal file
142
res/unpub-server/packages/unpub/README.md
Normal file
@ -0,0 +1,142 @@
|
||||
# Unpub
|
||||
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
## 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/d/{package_name}` |  |
|
||||
|
||||
## 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
|
||||
41
res/unpub-server/packages/unpub/bin/unpub.dart
Normal file
41
res/unpub-server/packages/unpub/bin/unpub.dart
Normal 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}');
|
||||
}
|
||||
15
res/unpub-server/packages/unpub/example/main.dart
Normal file
15
res/unpub-server/packages/unpub/example/main.dart
Normal 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}');
|
||||
}
|
||||
579
res/unpub-server/packages/unpub/lib/src/app.dart
Normal file
579
res/unpub-server/packages/unpub/lib/src/app.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
34
res/unpub-server/packages/unpub/lib/src/app.g.dart
Normal file
34
res/unpub-server/packages/unpub/lib/src/app.g.dart
Normal 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;
|
||||
}
|
||||
28
res/unpub-server/packages/unpub/lib/src/file_store.dart
Normal file
28
res/unpub-server/packages/unpub/lib/src/file_store.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
22
res/unpub-server/packages/unpub/lib/src/meta_store.dart
Normal file
22
res/unpub-server/packages/unpub/lib/src/meta_store.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
73
res/unpub-server/packages/unpub/lib/src/models.dart
Normal file
73
res/unpub-server/packages/unpub/lib/src/models.dart
Normal 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);
|
||||
}
|
||||
74
res/unpub-server/packages/unpub/lib/src/models.g.dart
Normal file
74
res/unpub-server/packages/unpub/lib/src/models.g.dart
Normal 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,
|
||||
};
|
||||
104
res/unpub-server/packages/unpub/lib/src/mongo_store.dart
Normal file
104
res/unpub-server/packages/unpub/lib/src/mongo_store.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
15
res/unpub-server/packages/unpub/lib/src/package_store.dart
Normal file
15
res/unpub-server/packages/unpub/lib/src/package_store.dart
Normal 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);
|
||||
}
|
||||
1707
res/unpub-server/packages/unpub/lib/src/static/index.html.dart
Normal file
1707
res/unpub-server/packages/unpub/lib/src/static/index.html.dart
Normal file
File diff suppressed because it is too large
Load Diff
16156
res/unpub-server/packages/unpub/lib/src/static/main.dart.js.dart
Normal file
16156
res/unpub-server/packages/unpub/lib/src/static/main.dart.js.dart
Normal file
File diff suppressed because one or more lines are too long
27
res/unpub-server/packages/unpub/lib/src/utils.dart
Normal file
27
res/unpub-server/packages/unpub/lib/src/utils.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
6
res/unpub-server/packages/unpub/lib/unpub.dart
Normal file
6
res/unpub-server/packages/unpub/lib/unpub.dart
Normal 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';
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
name: unpub_api
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
33
res/unpub-server/packages/unpub/pubspec.yaml
Normal file
33
res/unpub-server/packages/unpub/pubspec.yaml
Normal 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
|
||||
57
res/unpub-server/packages/unpub/test/file_store_test.dart
Normal file
57
res/unpub-server/packages/unpub/test/file_store_test.dart
Normal 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;
|
||||
}
|
||||
2
res/unpub-server/packages/unpub/test/fixtures/file_store/.gitignore
vendored
Normal file
2
res/unpub-server/packages/unpub/test/fixtures/file_store/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/CHANGELOG.md
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/CHANGELOG.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
# 0.0.1
|
||||
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/LICENSE
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/README.md
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/README.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
# package0 0.0.1
|
||||
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/pubspec.yaml
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.1/pubspec.yaml
vendored
Normal 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"
|
||||
3
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/CHANGELOG.md
vendored
Normal file
3
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/CHANGELOG.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# 0.0.2
|
||||
|
||||
# 0.0.1
|
||||
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/LICENSE
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/README.md
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/README.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
# package0 0.0.2
|
||||
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/pubspec.yaml
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.2/pubspec.yaml
vendored
Normal 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"
|
||||
7
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/CHANGELOG.md
vendored
Normal file
7
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/CHANGELOG.md
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# 0.0.3+1
|
||||
|
||||
# 0.0.3
|
||||
|
||||
# 0.0.2
|
||||
|
||||
# 0.0.1
|
||||
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/LICENSE
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/README.md
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/README.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
# package0 0.0.3+1
|
||||
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/pubspec.yaml
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3+1/pubspec.yaml
vendored
Normal 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"
|
||||
5
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/CHANGELOG.md
vendored
Normal file
5
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/CHANGELOG.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# 0.0.3
|
||||
|
||||
# 0.0.2
|
||||
|
||||
# 0.0.1
|
||||
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/LICENSE
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/README.md
vendored
Normal file
1
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/README.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
# package0 0.0.3
|
||||
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/pubspec.yaml
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/0.0.3/pubspec.yaml
vendored
Normal 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"
|
||||
0
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0-noreadme/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0-noreadme/LICENSE
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0-noreadme/pubspec.yaml
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0-noreadme/pubspec.yaml
vendored
Normal 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"
|
||||
0
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0/LICENSE
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0/pubspec.yaml
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_0/1.0.0/pubspec.yaml
vendored
Normal 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"
|
||||
0
res/unpub-server/packages/unpub/test/fixtures/package_1/0.0.1/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_1/0.0.1/LICENSE
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_1/0.0.1/README.md
vendored
Normal file
0
res/unpub-server/packages/unpub/test/fixtures/package_1/0.0.1/README.md
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_1/0.0.1/pubspec.yaml
vendored
Normal file
6
res/unpub-server/packages/unpub/test/fixtures/package_1/0.0.1/pubspec.yaml
vendored
Normal 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"
|
||||
407
res/unpub-server/packages/unpub/test/unpub_test.dart
Normal file
407
res/unpub-server/packages/unpub/test/unpub_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
57
res/unpub-server/packages/unpub/test/utils.dart
Normal file
57
res/unpub-server/packages/unpub/test/utils.dart
Normal 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});
|
||||
}
|
||||
14
res/unpub-server/packages/unpub/tool/pre_publish.dart
Normal file
14
res/unpub-server/packages/unpub/tool/pre_publish.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
9
res/unpub-server/pubspec.yaml
Normal file
9
res/unpub-server/pubspec.yaml
Normal 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"
|
||||
Reference in New Issue
Block a user