Copying All Gitea-Forgejo Repositories as a Zip

2 min read

I wanted to periodically grab a snapshot of the mainline/default branch of every repository on my locally hosted Forgejo Git server. I wrote the script below for Node 20+. It has no external dependencies. It’s not fancy, but it works for me.

Save it as a .mjs file (or use node --input-type=module scriptname.js).

You’ll need an Access token to grant the script privileges to scan your repositories and download the primary branch.

  • Go to your user’s access token Settings > Applications > Access Tokens
  • Then, provide the token a name (it’s just for your notes).
  • Select repository permission: read
  • Select user permission: read
  • Add your username and token as shown in the script.
  • Change the GITEA_HTTP_SERVER to the name of your git server. Mine is called sourced in this example.
  • Repeat the process for any other users you want to add.

Then, start the script. It will download to a subdirectory called repos/{username}. Or, you can modify the script to save to another location.

js
import { writeFile, mkdir } from "node:fs/promises";
import { Readable } from "node:stream";

const GITEA_HTTP_SERVER = "sourced";
// PUT the user name and an application token in the array of arrays:
// It might look like this:
// [  ["acorn", 3bd876af5a5629c31982900cd4f8956a469cccec" ]]
const TOKENS = [["username", "access-token"]];

async function getRepoNames(name, token) {
    const response = await fetch(`http://${GITEA_HTTP_SERVER}/api/v1/user/repos`, {
        method: "GET",
        headers: {
            Accept: "application/json",
            Authorization: `token ${token}`,
        },
    });
    const repos = await response.json();
    return repos.map((repo) => [repo.name, repo.default_branch]);
}

async function downloadRepo(username, token, repoName, branchName) {
    const response = await fetch(
        `http://${GITEA_HTTP_SERVER}/api/v1/repos/${username}/${repoName}/archive/${branchName}.zip`,
        {
            method: "GET",
            headers: {
                Accept: "application/zip",
                Authorization: `token ${token}`,
            },
        }
    );
    if (response.ok) {
        const stream = Readable.fromWeb(response.body);
        await mkdir(`./repos/${username}`, { recursive: true });
        await writeFile(`./repos/${username}/${repoName}.zip`, stream);
    } else {
        console.error(`Failed to download ${repoName}, ${response.statusText}`);
    }
}

for (const [name, token] of TOKENS) {
    const repoNames = await getRepoNames(name, token);
    for (const [repo, branchName] of repoNames) {
        await downloadRepo(name, token, repo, branchName);
    }
}

Hi! Before you go...🙏

I really appreciate you stopping by and reading my blog!

You might not know that each Epic blog post takes me several hours to write and edit.

If you could help me by using my Amazon affiliate links, it would further encourage me to write these stories for you (and help justify the time spent). As always, the links don't add cost to the purchase you're making, I'll just get a little something from Amazon as a thanks.

I'll occasionally write a blog post with a recommendation and I've also added a page dedicated to some of my more well-liked things. While you can buy something I've recommended, you can also just jump to Amazon and make a purchase. Thanks again!