exec method

  1. @override
Future<void> exec(
  1. ExecContext context
)
override

Run command.

The contents of katana.yaml and the arguments of the command are passed to context.

コマンドを実行します。

contextkatana.yamlの内容やコマンドの引数が渡されます。

Implementation

@override
Future<void> exec(ExecContext context) async {
  final bin = context.yaml.getAsMap("bin");
  final npm = bin.get("npm", "npm");
  final wrangler = bin.get("wrangler", "wrangler");
  final flavor = context.flavorContext?.flavor.name ?? "prod";
  final cloudflare = context.yaml.getAsMap("cloudflare");
  final storage = cloudflare.getAsMap("storage");
  final binding = storage.get("binding", "R2_BUCKET");
  final bucketName = storage.get("bucket_name", "");
  final previewBucketName = storage.get("preview_bucket_name", "");
  final publicBaseUrl = storage.get("public_base_url", "");
  final configuredDownloadUrlSecret = storage.get("download_url_secret", "");
  final backup = storage.getAsMap("backup");
  final backupEnabled = backup.get("enable", false);
  final configuredConsumerFlavor =
      backup.get("consumer_flavor", "").toString().trim();
  final backupBinding = backup.get("binding", "R2_BACKUP_BUCKET");
  final backupBucketName = backup.get("bucket_name", "");
  final backupPreviewBucketName = backup.get("preview_bucket_name", "");
  final backupQueueName = backup.get(
    "queue_name",
    bucketName.isEmpty ? "" : "$bucketName-backup",
  );
  final backupMaxBatchSize = backup.get("max_batch_size", 10);
  final backupMaxBatchTimeout = backup.get("max_batch_timeout", 5);
  final backupMaxRetries = backup.get("max_retries", 3);
  final backupDeadLetterQueue = backup.get(
    "dead_letter_queue",
    backupQueueName.isEmpty ? "" : "$backupQueueName-dlq",
  );
  if (binding.isEmpty) {
    error(
      "If [cloudflare]->[storage]->[enable] is enabled, please include [cloudflare]->[storage]->[binding].",
    );
    return;
  }
  if (bucketName.isEmpty) {
    error(
      "If [cloudflare]->[storage]->[enable] is enabled, please include [cloudflare]->[storage]->[bucket_name]. Create it with `wrangler r2 bucket create <bucket_name>`.",
    );
    return;
  }
  if (publicBaseUrl.isEmpty) {
    error(
      "If [cloudflare]->[storage]->[enable] is enabled, please include [cloudflare]->[storage]->[public_base_url].",
    );
    return;
  }
  if (backupEnabled) {
    if (configuredConsumerFlavor.isNotEmpty &&
        configuredConsumerFlavor != KatanaFlavor.dev.name &&
        configuredConsumerFlavor != KatanaFlavor.prod.name) {
      error(
        "Cloudflare R2 backup [consumer_flavor] must be `dev` or `prod`.",
      );
      return;
    }
    if (backupBinding.isEmpty) {
      error(
        "If [cloudflare]->[storage]->[backup]->[enable] is enabled, please include [binding].",
      );
      return;
    }
    if (backupBucketName.isEmpty) {
      error(
        "If [cloudflare]->[storage]->[backup]->[enable] is enabled, please include [bucket_name]. Create it with `wrangler r2 bucket create <bucket_name>`.",
      );
      return;
    }
    if (backupQueueName.isEmpty) {
      error(
        "If [cloudflare]->[storage]->[backup]->[enable] is enabled, please include [queue_name].",
      );
      return;
    }
    if (binding == backupBinding || bucketName == backupBucketName) {
      error(
        "Cloudflare R2 backup must use a different binding and bucket from the source storage.",
      );
      return;
    }
    if (backupMaxBatchSize <= 0 ||
        backupMaxBatchTimeout < 0 ||
        backupMaxRetries < 0) {
      error(
        "Cloudflare R2 backup queue settings must be non-negative, and [max_batch_size] must be greater than zero.",
      );
      return;
    }
  }
  final cloudflareDir = Directory("cloudflare");
  if (!cloudflareDir.existsSync()) {
    error(
      "The directory `cloudflare` does not exist. Initialize Cloudflare Workers by enabling [cloudflare]->[workers]->[enable] and executing `katana apply`.",
    );
    return;
  }
  final indexFile = File("cloudflare/src/index.ts");
  if (!indexFile.existsSync()) {
    error(
      "The file `cloudflare/src/index.ts` does not exist. Initialize Cloudflare Workers by enabling [cloudflare]->[workers]->[enable] and executing `katana apply`.",
    );
    return;
  }
  final wranglerFile = File("cloudflare/wrangler.jsonc");
  if (!wranglerFile.existsSync()) {
    error(
      "The file `cloudflare/wrangler.jsonc` does not exist. Initialize Cloudflare Workers by enabling [cloudflare]->[workers]->[enable] and executing `katana apply`.",
    );
    return;
  }
  try {
    await ensureStorageManagedStateIsGitIgnored();
  } on StateError catch (exception) {
    error(exception.message.toString());
    return;
  }
  final secrets = await _loadSecretsRoot();
  late final StorageManagedStateLoadResult managed;
  try {
    managed = await loadAndMigrateStorageManagedState(
      secrets,
      configuredSecret: configuredDownloadUrlSecret,
    );
  } on StateError catch (exception) {
    error(exception.message.toString());
    return;
  }
  if (managed.stateChanged) {
    await saveStorageManagedState(managed.state);
  }
  if (managed.secretsChanged) {
    await _saveSecretsRoot(secrets);
  }
  final downloadUrlSecret = managed.downloadUrlSecret;
  final ownsBackupConsumer = _ownsBackupQueueConsumer(
    context,
    queueName: backupQueueName,
    configuredConsumerFlavor: configuredConsumerFlavor,
  );

  label("Add Cloudflare Workers functions");
  final source = await indexFile.readAsString();
  final updated = _updateStorageFunctions(
    source,
    binding: binding,
    bucketName: bucketName,
    publicBaseUrl: publicBaseUrl,
    backupEnabled: backupEnabled,
    backupBinding: backupBinding,
  );
  if (updated == null) {
    return;
  }
  await indexFile.writeAsString(updated);
  label("Add Cloudflare R2 bucket binding");
  final wranglerSource = WranglerEnvironmentSynchronizer.transformEnvironment(
    WranglerEnvironmentSynchronizer.ensureEnvironment(
      await wranglerFile.readAsString(),
      flavor: flavor,
      workerName: cloudflare.get("project_id", ""),
    ),
    flavor: flavor,
    transform: (environment) {
      var updated = _updateWranglerR2Bucket(
        environment,
        binding: binding,
        bucketName: bucketName,
        previewBucketName: previewBucketName,
        backupEnabled: backupEnabled,
        backupBinding: backupBinding,
        backupBucketName: backupBucketName,
        backupPreviewBucketName: backupPreviewBucketName,
      );
      if (backupEnabled) {
        updated = ownsBackupConsumer
            ? _updateWranglerQueueConsumer(
                updated,
                queueName: backupQueueName,
                maxBatchSize: backupMaxBatchSize,
                maxBatchTimeout: backupMaxBatchTimeout,
                maxRetries: backupMaxRetries,
                deadLetterQueue: backupDeadLetterQueue,
              )
            : _removeWranglerQueueConsumer(
                updated,
                queueName: backupQueueName,
              );
      }
      return updated;
    },
  );
  await wranglerFile.writeAsString(wranglerSource);
  await installMissingCloudflarePackages(
    npm: npm,
    packages: const ["@mathrunet/masamune_cloudflare_storage"],
  );
  await addFlutterImport(
    [
      "masamune_storage_cloudflare",
    ],
  );
  await _putWranglerSecret(
    wrangler: wrangler,
    environment: flavor,
    key: "STORAGE_DOWNLOAD_URL_SECRET",
    value: downloadUrlSecret,
  );
  if (backupEnabled) {
    await _ensureQueue(wrangler: wrangler, queueName: backupQueueName);
    if (backupDeadLetterQueue.isNotEmpty) {
      await _ensureQueue(
        wrangler: wrangler,
        queueName: backupDeadLetterQueue,
      );
    }
    await _ensureR2CreateNotification(
      wrangler: wrangler,
      bucketName: bucketName,
      queueName: backupQueueName,
    );
  }
}