zapstore / zapstore-cli

The permissioness package manager
https://zapstore.dev
MIT License
13 stars 1 forks source link

Support Gitlab #15

Open franzaps opened 6 months ago

franzaps commented 6 months ago

Directories:

franzaps commented 4 months ago

For gitlab:

  const repoUrl = `https://gitlab.com/api/v4/projects/${decodeURIComponent(repo)}`;
  const latestReleaseUrl = `https://gitlab.com/api/v4/projects/${decodeURIComponent(repo)}/releases/permalink/latest`;
"links": I
"id": 4313441,
"name": "barcode-scanner-1.24.0.apk",
"url": "https://gitlab.com/api/v4/projects/31308089/packages/generic/release/1.24.0/barcode-scanner-1.24.0.apk"
"direct_asset_url": "https://gitlab.com/Atharok/BarcodeScanner/-/releases/1.24.0/downloads/barcode-scanner-1.24.0.apk",
"link_type": "other"
]
franzaps commented 2 weeks ago

Unfinished code (should factor out lots of common methods)

import 'package:cli_spin/cli_spin.dart';
import 'package:collection/collection.dart';
import 'package:purplebase/purplebase.dart';
import 'package:zapstore_cli/models/nostr.dart';
import 'package:http/http.dart' as http;
import 'package:zapstore_cli/utils.dart';

class GitlabParser extends RepositoryParser {
  final RelayMessageNotifier relay;

  GitlabParser({required this.relay});

  @override
  Future<(App, Release, Set<FileMetadata>)> run({
    required App app,
    required String os,
    required bool overwriteApp,
    required bool overwriteRelease,
    String? repoName,
    Map<String, dynamic>? artifacts,
    String? artifactContentType,
  }) async {
    final metadataSpinner = CliSpin(
      text: 'Fetching metadata...',
      spinner: CliSpinners.dots,
    ).start();

    repoName = repoName!.replaceFirst('/', '%2F');

    final latestReleaseUrl =
        'https://gitlab.com/api/v4/projects/$repoName/releases/permalink/latest';
    var latestReleaseJson =
        await http.get(Uri.parse(latestReleaseUrl)).getJson();

    final assets = latestReleaseJson['assets'] as Iterable;
    final packageAssetArray = artifactContentType != null
        ? assets.where((a) {
            return a.content_type == artifactContentType;
          })
        : assets;

    if (packageAssetArray.isEmpty) {
      metadataSpinner.fail('No packages in $repoName, I\'m done here');
      throw GracefullyAbortSignal();
    }

    metadataSpinner.success('Fetched metadata from Github');

    final fileMetadatas = <FileMetadata>{};
    for (var MapEntry(key: regexpKey, :value) in artifacts!.entries) {
      regexpKey = regexpKey.replaceAll('%v', r'(\d+\.\d+(\.\d+)?)');
      final r = RegExp(regexpKey);
      final asset = assets.firstWhereOrNull((a) => r.hasMatch(a['name']));

      if (asset == null) {
        throw 'No asset matching ${r.pattern}';
      }

      final packageUrl = asset['browser_download_url'];

      final packageSpinner = CliSpin(
        text: 'Fetching package: $packageUrl...',
        spinner: CliSpinners.dots,
      ).start();

      // Check if we already processed this release
      final metadataOnRelay =
          await relay.query<FileMetadata>(search: packageUrl);

      // Search is full-text (not exact) so we double-check
      final metadataOnRelayCheck = metadataOnRelay
          .firstWhereOrNull((m) => m.urls.firstOrNull == packageUrl);
      if (metadataOnRelayCheck != null) {
        if (!overwriteRelease) {
          packageSpinner
              .fail('Latest $repoName release already in relay, nothing to do');
          throw GracefullyAbortSignal();
        }
      }

      final tempPackagePath =
          await fetchFile(packageUrl, spinner: packageSpinner);

      final match = r.firstMatch(asset['name']);
      final matchedVersion = (match?.groupCount ?? 0) > 0
          ? r.firstMatch(asset['name'])?.group(1)
          : latestReleaseJson['tag_name'];

      // Validate platforms
      final platforms = {...?value['platforms'] as Iterable?};
      if (!platforms
          .every((platform) => kSupportedPlatforms.contains(platform))) {
        throw 'Artifact ${asset['name']} has platforms $platforms but some are not in $kSupportedPlatforms';
      }

      final (fileHash, filePath, _) = await renameToHash(tempPackagePath);
      final size = await runInShell('wc -c < $filePath');
      final fileMetadata = FileMetadata(
          content: '${app.name} ${latestReleaseJson['tag_name']}',
          createdAt: DateTime.tryParse(latestReleaseJson['created_at']),
          urls: {packageUrl},
          mimeType: asset['content_type'],
          hash: fileHash,
          size: int.tryParse(size),
          platforms: platforms.toSet().cast(),
          version: latestReleaseJson['tag_name'],
          pubkeys: app.pubkeys,
          zapTags: app.zapTags,
          additionalEventTags: {
            for (final b in (value['executables'] ?? []))
              (
                'executable',
                matchedVersion != null
                    ? b.toString().replaceFirst('%v', matchedVersion)
                    : b
              ),
          });
      fileMetadata.transientData['apkPath'] = filePath;
      fileMetadatas.add(fileMetadata);
      packageSpinner.success('Fetched package: $packageUrl');
    }

    if (overwriteApp) {
      final repoUrl = 'https://api.github.com/repos/$repoName';
      final repoJson = await http.get(Uri.parse(repoUrl)).getJson();

      app = app.copyWith(
        content: app.content ?? repoJson['description'] ?? repoJson['name'],
        identifier: app.identifier ?? repoJson['name'],
        name: app.name ?? repoJson['name'],
        url: app.url ??
            ((repoJson['homepage']?.isNotEmpty ?? false)
                ? repoJson['homepage']
                : null),
        repository: app.repository ?? 'https://github.com/$repoName',
        license: app.license ?? repoJson['license']?['spdx_id'],
        tags: app.tags.isEmpty
            ? (repoJson['topics'] as Iterable).toSet().cast()
            : app.tags,
        pubkeys: app.pubkeys,
        zapTags: app.zapTags,
      );
    }

    final release = Release(
      createdAt: DateTime.tryParse(latestReleaseJson['created_at']),
      content: latestReleaseJson['body'],
      identifier: '${app.identifier}@${latestReleaseJson['tag_name']}',
      url: latestReleaseJson['html_url'],
      pubkeys: app.pubkeys,
      zapTags: app.zapTags,
    );

    return (app, release, fileMetadatas);
  }
}

abstract class RepositoryParser {
  Future<(App, Release, Set<FileMetadata>)> run({
    required App app,
    required String os,
    required bool overwriteApp,
    required bool overwriteRelease,
  });
}