One Click to Optimize Images, Create Repo and Making Commit
Tags: myphotohub, opensource, javascript, octokit
Published: 2022-12-11 01:14:32

Continuing from last post, I have completed my feature that produces optimized images, create a remote repository and make a commit with the files. My [PR](https://github.com/humphd/my-photohub/pull/30) may be updated, but I think the change would not be so big from now. 

So the main challenge was how I can make one click to create a repo and make a commit. I tried to use [`octokit.rest`](https://octokit.github.io/rest.js/v19) library. I'm not sure what I did wrong, but it didn't read the parameters that I passed. But [GitHub API version](https://docs.github.com/en/rest/commits/comments?apiVersion=2022-11-28#create-a-commit-comment) works for me. And the post [How to push files programatically to a repository using Octokit with Typescript](https://dev.to/lucis/how-to-push-files-programatically-to-a-repository-using-octokit-with-typescript-1nj0) was a huge help for me. My version is not so different from this post except that I use `JavaScript` and create multiple blobs in the middle of the process. 

First I create a remote repo with `createRepo` function below. 

```js
const createRepo = async (octokit, username, repoName) => {
  try {
    await octokit.rest.repos.createForAuthenticatedUser({
      name: repoName ? repoName : generateUniqueName(username),
      description: 'Your repository generated using my-photohub',
      private: false,
      auto_init: true,
    });
    return true;
  } catch (err) {
    console.error(err);
    return false;
  }
};
``` 

Then, I produce optimized images with `compressor.js`. 

```js
const compressImage = (file, option) => {
  return new Promise((resolve, reject) => {
    new Compressor(file, {
      width: option.width,
      quality: 0.6,
      success: (result) => {
        const fileName =
          file.name.replace(/\.[^/.]+$/, '') +
          '_' +
          option.width +
          '.' +
          option.mimeType.substring(option.mimeType.indexOf('/') + 1);
        resolve(new File([result], fileName, { type: option.mimeType }));
      },
      error: (err) => {
        console.error(err.message);
        reject(err);
      },
    });
  });
};

export const createOptimizedImages = async (originalImages) => {
  if (!originalImages) {
    return;
  }

  let compressFilesPromises = [];
  compressorOptions.forEach((option) => {
    for (const image of originalImages) {
      compressFilesPromises.push(compressImage(image, option));
    }
  });

  return Promise.all(compressFilesPromises);
};
```
You can change image quality or pass different options. I set customized `mimeType` and `width` by passing option parameter here. You can find more usages from [`compressor.js` original documentation](https://github.com/fengyuanchen/compressorjs/blob/main/README.md). 

Then I need to create a blob from each file with the function below. 

```js
const createBlobForFile = async (octokit, username, repoName, files) => {
  let blobDataPromises = [];
  for (const file of files) {
    let reader = new FileReader();
    await reader.readAsDataURL(file);
    const promise = new Promise((resolve, reject) => {
      reader.onload = async () => {
        const base64Data = reader.result;
        const blobData = octokit.request(`POST /repos/${username}/${repoName}/git/blobs`, {
          owner: username,
          repo: repoName,
          content: base64Data,
        });
        resolve(blobData);
      };
      reader.onerror = reject;
    });
    blobDataPromises.push(promise);
  }
  return Promise.all(blobDataPromises);
};
```

So this function returns an array of blob object in the same order from files.


> **_NOTE:_**  You can also use `BlobURL` as `DataURL` is very long and it has a length limit. ```js
reader.readAsArrayBuffer(file);
...
const imageBuffer = reader.result;
const byteArray = new Uint8Array(imageBuffer);
const blob = new Blob([byteArray]);
const BlobURL = URL.createObjectURL(blob);
```



In the blob, it doesn't have a file name that I can use as a path in next function. So I added `getPathNamesFromFile()`.

```js
const getPathNamesFromFile = (convertedFiles) => {
  let pathNames = [];
  for (const file of convertedFiles) {
    pathNames.push(file.name);
  }
  return pathNames;
};
```

Next step is that I need to get current existing commit from the repo that I just created so that I can add a new commit as a child. Here's the function. 

```js
const createNewCommit = async (
  octokit,
  username,
  repoName,
  message,
  currentTreeSha,
  currentCommitSha
) =>
  (
    await octokit.request(`POST /repos/${username}/${repoName}/git/commits`, {
      owner: username,
      repo: repoName,
      message: message,
      tree: currentTreeSha,
      parents: [currentCommitSha],
    })
  ).data;
```

The function is pretty obvious as if I'm making an actual commit. Then, I need to create trees to add. You can find details of `git tree object` [here](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#_tree_objects). Basically `blob` is a file and `tree` is a directory in `git`. You can check your `git` folder with this command. 

```sh
❯ git cat-file -p main^{tree}                                                ─╯
...
040000 tree f808c5b1a9eaa36928f49ac82b8b2ed85c53af45	.vscode
100644 blob 3ee245e88d83084868e58ba369200acc8354f9df	CONTRIBUTING.md
...
100644 blob 8202e8c8292a2681114b67099e5f47c5aa0e13e4	package.json
040000 tree 2ce2a0fdb03a853cc6c855ebc46865cd20d3afaf	public
040000 tree e3ef837d95be8981e826c7ad683101475f2b6528	src
...
```

And you would have noticed numbers in front of `tree` and `blob`. This number is an associated mode with the file. `100644` means a normal file. You would have figured `040000` is a directory. `100755` is an executable file and `120000` is a symbolic link. 

And the last step is to set the branch to this new commit. 

```js
const setBranchToCommit = (octokit, username, repoName, branch = `main`, commitSha) =>
  octokit.request(`PATCH /repos/${username}/${repoName}/git/refs/heads/${branch}`, {
    owner: username,
    repo: repoName,
    ref: `heads/${branch}`,
    sha: commitSha,
  });
```

## Conclusion 
I spent many days to figure this out. But in the end, I was so happy to see my code working as intended. I was on the issue that my array of promises in `createBlobForFile()` returned an empty array for many hours. I could have fixed the problem if I tried to think more rational about the behaviour and root cause. I was adding each promise inside `reader.onload`. So the outer function just bypassed the code block and returned an empty array immediately. It was a lot of trials and learning. Moreover, I'm so glad that I set very fundamental of this app.