DEV Community

Genne23v
Genne23v

Posted on

One Click to Optimize Images, Create Repo and Making Commit

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 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 library. I'm not sure what I did wrong, but it didn't read the parameters that I passed. But GitHub API version works for me. And the post How to push files programatically to a repository using Octokit with Typescript 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.

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

Then, I produce optimized images with compressor.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);
};
Enter fullscreen mode Exit fullscreen mode

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.

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

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);
};
Enter fullscreen mode Exit fullscreen mode

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.

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().

const getPathNamesFromFile = (convertedFiles) => {
  let pathNames = [];
  for (const file of convertedFiles) {
    pathNames.push(file.name);
  }
  return pathNames;
};
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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. Basically blob is a file and tree is a directory in git. You can check your git folder with this command.

โฏ 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
...
Enter fullscreen mode Exit fullscreen mode

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.

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,
  });
Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (0)