Gas. Ini upgrade yang memang wajib.

Sekarang upload-r2.js kita bikin lebih pintar:

  • cek object di R2 dulu
  • kalau file sudah ada → skip
  • kalau belum ada → upload

Jadi publish berikutnya jauh lebih cepat.

Replace upload-r2.js dengan versi ini

import "dotenv/config";
import fs from "fs";
import path from "path";
import {
  S3Client,
  PutObjectCommand,
  HeadObjectCommand
} from "@aws-sdk/client-s3";

const mediaDir = "./media";

const {
  R2_ACCOUNT_ID,
  R2_ACCESS_KEY_ID,
  R2_SECRET_ACCESS_KEY,
  R2_BUCKET
} = process.env;

if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !R2_BUCKET) {
  console.error("Env R2 belum lengkap. Cek .env");
  process.exit(1);
}

const client = new S3Client({
  region: "auto",
  endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: R2_ACCESS_KEY_ID,
    secretAccessKey: R2_SECRET_ACCESS_KEY
  }
});

function getContentType(fileName) {
  const ext = path.extname(fileName).toLowerCase();

  if (ext === ".mp4") return "video/mp4";
  if (ext === ".m3u8") return "application/vnd.apple.mpegurl";
  if (ext === ".ts") return "video/mp2t";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".png") return "image/png";
  if (ext === ".webp") return "image/webp";
  if (ext === ".gif") return "image/gif";
  if (ext === ".json") return "application/json";

  return "application/octet-stream";
}

async function objectExists(key) {
  try {
    await client.send(
      new HeadObjectCommand({
        Bucket: R2_BUCKET,
        Key: key
      })
    );
    return true;
  } catch (err) {
    const code = err?.name || err?.Code || err?.code;
    const status = err?.$metadata?.httpStatusCode;

    if (
      code === "NotFound" ||
      code === "NoSuchKey" ||
      status === 404
    ) {
      return false;
    }

    throw err;
  }
}

async function uploadFile(fileName) {
  const fullPath = path.join(mediaDir, fileName);

  if (!fs.statSync(fullPath).isFile()) return;

  const exists = await objectExists(fileName);

  if (exists) {
    console.log(`Skip (exists): ${fileName}`);
    return;
  }

  const body = fs.readFileSync(fullPath);

  const command = new PutObjectCommand({
    Bucket: R2_BUCKET,
    Key: fileName,
    Body: body,
    ContentType: getContentType(fileName)
  });

  await client.send(command);
  console.log(`Uploaded: ${fileName}`);
}

async function main() {
  const files = fs.readdirSync(mediaDir);

  for (const file of files) {
    await uploadFile(file);
  }

  console.log("R2 upload selesai.");
}

main().catch(err => {
  console.error("Upload gagal:", err);
  process.exit(1);
});

Cara test

Jalankan:

npm run r2

Kalau file sudah pernah diupload, harus muncul seperti:

Skip (exists): ade_manisss02_2022-05-10_10-01-23.mp4
Skip (exists): ade_manisss02_2022-05-10_10-01-23.jpg

Kalau file baru, harus muncul:

Uploaded: namafilebaru.mp4

Penting

Versi ini skip berdasarkan:

  • nama file / key
  • bukan ukuran
  • bukan checksum

Artinya kalau kamu ganti isi file tapi nama file tetap sama, script akan anggap file itu sudah ada dan di-skip.

Jadi sementara aturannya:

Kalau isi file berubah, ubah nama file juga.

Contoh:

video1.mp4
video1-v2.mp4

Itu aman.

Kalau kamu mau lebih pintar nanti

Kita bisa upgrade lagi jadi:

  • cek size lokal vs size object R2
  • atau pakai hash/checksum

Tapi sekarang ini sudah cukup bagus dan simpel.

Step berikutnya yang paling pas

Setelah skip upload beres, upgrade terbaik adalah:

auto release satu command penuh

jadi cukup:

npm run release -- "add new videos"

dan semuanya jalan sekaligus.

Kalau mau, next saya bikinkan release.js yang sekalian:

  • upload R2
  • generate data
  • git add
  • git commit
  • git push