code icon Code

Upload Image to GitHub Pages

Upload an image file to GitHub Pages and return a public URL. Uses a shared media-assets repo with date-based organization.

Source Code

import fs from "fs";
import path from "path";

const REPO_NAME = "media-assets";

const [imagePath, customFilename = "", trackingPath] = process.argv.slice(2);

// Validate inputs
if (!imagePath) {
  console.error("Error: imagePath is required");
  process.exit(1);
}

if (!fs.existsSync(imagePath)) {
  console.error(`Error: File not found: ${imagePath}`);
  process.exit(1);
}

// Get file info
const originalExt = path.extname(imagePath).toLowerCase();
const originalName = path.basename(imagePath, originalExt);
const supportedExts = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"];

if (!supportedExts.includes(originalExt)) {
  console.error(
    `Error: Unsupported file type: ${originalExt}. Supported: ${supportedExts.join(", ")}`
  );
  process.exit(1);
}

// Generate filename
const timestamp = Date.now();
const sanitizedCustomName = customFilename
  ? customFilename
      .toLowerCase()
      .replace(/[^a-z0-9-]/g, "-")
      .replace(/-+/g, "-")
      .replace(/^-|-$/g, "")
  : "";

const finalFilename = sanitizedCustomName
  ? `${sanitizedCustomName}${originalExt}`
  : `${originalName}-${timestamp}${originalExt}`;

// Date-based path: YYYY/MM/DD/filename
const now = new Date();
const datePath = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}`;
const remotePath = `${datePath}/${finalFilename}`;

// Read file and convert to base64
const fileBuffer = fs.readFileSync(imagePath);
const contentBase64 = fileBuffer.toString("base64");
const fileSize = fileBuffer.length;

console.log(`Uploading image to GitHub Pages...`);
console.log(`  Source: ${imagePath}`);
console.log(`  Size: ${fileSize} bytes`);
console.log(`  Remote path: ${remotePath}`);

// GitHub API helper
async function githubFetch(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      Authorization: "Bearer PLACEHOLDER_TOKEN",
      Accept: "application/vnd.github.v3+json",
      "User-Agent": "Sauna-Agent",
      ...options.headers,
    },
  });
  return response;
}

try {
  // Step 1: Get authenticated user
  const userResponse = await githubFetch("https://api.github.com/user");

  if (!userResponse.ok) {
    const error = await userResponse.text();
    console.error("Failed to authenticate with GitHub");
    console.error(error);
    throw new Error("GitHub authentication failed");
  }

  const user = await userResponse.json();
  const username = user.login;
  console.log(`  Authenticated as: ${username}`);

  // Step 2: Check if media-assets repo exists
  const repoCheckResponse = await githubFetch(
    `https://api.github.com/repos/${username}/${REPO_NAME}`
  );

  let repoExists = repoCheckResponse.ok;

  // Step 3: Create repo if it doesn't exist
  if (!repoExists) {
    console.log(`  Repository ${REPO_NAME} does not exist. Creating...`);

    const createRepoResponse = await githubFetch(
      "https://api.github.com/user/repos",
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: REPO_NAME,
          description: "Hosted media assets for Sauna skills",
          homepage: `https://${username}.github.io/${REPO_NAME}`,
          private: false,
          auto_init: true,
          has_pages: true,
        }),
      }
    );

    if (!createRepoResponse.ok) {
      const error = await createRepoResponse.text();
      console.error("Failed to create repository");
      console.error(error);
      throw new Error("Repository creation failed");
    }

    console.log("  Repository created successfully");

    // Wait for repo initialization
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Enable GitHub Pages
    console.log("  Enabling GitHub Pages...");
    try {
      await githubFetch(
        `https://api.github.com/repos/${username}/${REPO_NAME}/pages`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            source: {
              branch: "main",
              path: "/",
            },
          }),
        }
      );
      console.log("  GitHub Pages enabled");
    } catch {
      console.log("  Note: GitHub Pages may need manual activation");
    }
  } else {
    console.log(`  Repository ${REPO_NAME} exists`);
  }

  // Step 4: Check for existing file to get SHA (for updates)
  const getFileResponse = await githubFetch(
    `https://api.github.com/repos/${username}/${REPO_NAME}/contents/${remotePath}`
  );

  let sha = null;
  if (getFileResponse.ok) {
    const fileData = await getFileResponse.json();
    sha = fileData.sha;
    console.log("  Updating existing file");
  } else {
    console.log("  Creating new file");
  }

  // Step 5: Upload/update file
  const uploadBody = {
    message: `Upload ${finalFilename}`,
    content: contentBase64,
  };

  if (sha) {
    uploadBody.sha = sha;
  }

  const uploadResponse = await githubFetch(
    `https://api.github.com/repos/${username}/${REPO_NAME}/contents/${remotePath}`,
    {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(uploadBody),
    }
  );

  if (!uploadResponse.ok) {
    const error = await uploadResponse.text();
    console.error("Failed to upload file");
    console.error(error);
    throw new Error("File upload failed");
  }

  const result = await uploadResponse.json();

  // Output results
  const imageUrl = `https://${username}.github.io/${REPO_NAME}/${remotePath}`;
  const repoUrl = `https://github.com/${username}/${REPO_NAME}`;

  console.log("\nโœ… Uploaded successfully!");
  console.log(`\n๐Ÿ“ Image URL: ${imageUrl}`);
  console.log(`๐Ÿ“ Repository: ${repoUrl}`);
  console.log(`๐Ÿ“ Commit: ${result.commit.sha.substring(0, 7)}`);
  console.log(
    "\nNote: GitHub Pages may take 1-2 minutes to serve new images."
  );

  // Output structured result for downstream use
  const output = {
    success: true,
    url: imageUrl,
    repository: repoUrl,
    filename: finalFilename,
    remotePath: remotePath,
    username: username,
    commit: result.commit.sha,
    size: fileSize,
    timestamp: new Date().toISOString(),
  };

  console.log("\n--- RESULT ---");
  console.log(JSON.stringify(output, null, 2));

  // Step 6: Update tracking file if provided
  if (trackingPath) {
    let images = [];
    if (fs.existsSync(trackingPath)) {
      images = JSON.parse(fs.readFileSync(trackingPath, "utf-8"));
    }

    images.push({
      url: imageUrl,
      filename: finalFilename,
      remotePath: remotePath,
      size: fileSize,
      uploadedAt: output.timestamp,
    });

    fs.writeFileSync(trackingPath, JSON.stringify(images, null, 2));
    console.log(`\n๐Ÿ“‹ Updated tracking: ${trackingPath}`);
  }
} catch (error) {
  console.error(`\nโŒ Upload failed: ${error.message}`);
  process.exit(1);
}