Blog update - using pre-commit hook to upload local media to supabase bucket
Replacing the manual steps of uploading media and replacing the media references in my blog markdown files
This blog post outlines the steps I took to automate uploading local media to Supabase and referencing the remote file.
When I first started this markdown based blog, I referenced local images and uploaded them to github. I realized this was a problem when I tried adding a large number of images to git and received the error that my commit was too large - filesizewise.
I migrated to using Supabase Buckets - a user friendly wrapper on top of an Amazon S3 bucket - to upload my media to and reference in my posts. This way, I'm not committing my media - just the remote reference to it. This adds a few additional steps though - manually uploading the file to Supabase, copying the URL and referencing the remote url in my markdown. This automates that process using a node script and a pre-commit hook that will run when I fire off a git commit command.
My development steps as as follow - using Bun to run TypeScript
upload media files to Supabase bucket
find local media references
replace local media references with remote references in blog
delete local media files
Put together full script to find local files, upload them to Supabase, replace the local references to remote and delete all the local files
This function reads the file content, constructs the upload path, and uses the Supabase client to upload the file to the specified bucket. It handles both successful uploads and potential errors, returning a status object with relevant information.
Step 2 - Finding Local Media References
The findLocalMediaReferences function scans Markdown files for local media references:
1import fs from"fs/promises";2import path from"path";34asyncfunctionfindLocalMediaReferences(contentDir:string):Promise<5{6 fullPath:string;7 destinationDirName:string;8}[]9>{10const mediaFiles:{11 fullPath:string;12 destinationDirName:string;13}[]=[];1415// Find all .md files in the content directory16const allFiles =awaitwalkDir(contentDir);17const mdFiles = allFiles.filter((file)=> path.extname(file)===".md");1819for(const file of mdFiles){20const content =await fs.readFile(file,"utf-8");2122// Regular expression to match Markdown image and link syntax23const mediaRegex =24/!\[.*?\]\(((?!http|https:\/\/).*?)\)|(?<!!)\[.*?\]\(((?!http|https:\/\/).*?)\)/g;2526const destinationDirName = file.split("/")[1].split(".")[0];2728let match;29while((match = mediaRegex.exec(content))!==null){30const mediaPath = match[1]|| match[2];31if(isMediaFile(mediaPath)){32const fullPath = path
33.resolve(path.dirname(file), mediaPath)34.split("/")35.slice(1)36.join("/")37.split(" ")[0];38 mediaFiles.push({ fullPath, destinationDirName });39}40}41}4243return mediaFiles;44}
typescript
This function uses a regular expression to find local media references in Markdown files, excluding external URLs. It returns an array of objects containing the full path and destination directory name for each media file.
Step 3 - Replacing Local Media References with Remote References
The replaceLocalReferenceWithRemote function updates the Markdown files to use the new remote URLs:
javascript
Copy
1asyncfunctionwalkDir(dir:string):Promise<string[]>{2const files =await fs.readdir(dir);3const paths =awaitPromise.all(4 files.map(async(file)=>{5const filePath = path.join(dir, file);6const stats =await fs.stat(filePath);7if(stats.isDirectory())returnwalkDir(filePath);8elseif(stats.isFile())return filePath;9return[];10})11);12return paths.flat();13}1415asyncfunctionreplaceLocalReferenceWithRemote(16 status:17|{18 status:"success";19 localFilepath:string;20 remoteFilepath:string;21}22|{23 status:"error";24 localFilepath:string;25 message:string;26}27){28if(status.status !=="success"){29console.log(30`Skipping update for ${status.localFilepath} due to non-success status`31);32return;33}3435const contentDir ="./blog";// Adjust this to your content directory36const allFiles =awaitwalkDir(contentDir);37const mdFiles = allFiles.filter((file)=> path.extname(file)===".md");3839for(const file of mdFiles){40let content =await fs.readFile(file,"utf-8");4142// Escape special characters in the local filepath for use in regex43const filename =44 status.localFilepath.split("/")[45 status.localFilepath.split("/").length -146];47const escapedLocalPath = filename.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");4849// Create a regex that matches the local filepath, accounting for possible relative paths and an optional preceding slash50const localPathRegex =newRegExp(51`(\\.\\.?\\/)*\\/?${escapedLocalPath}`,52"g"53);5455if(localPathRegex.test(content)){56// Replace all occurrences of the local path with the remote path, preserving any preceding slash57 content = content.replace(localPathRegex,(match)=>{58const hasLeadingSlash = match.startsWith("/");59return hasLeadingSlash
60?`/${status.remoteFilepath}`61: status.remoteFilepath;62});6364// Write updated content back to the file65await fs.writeFile(file, content,"utf-8");66console.log(67`Updated references to ${status.localFilepath} in file: ${file}`68);69deleteLocalFile(`/${status.localFilepath}`);70}71}72}
typescript
This function replaces all occurrences of the local file path with the new remote URL in the Markdown files, preserving any leading slashes.
Step 4 - Deleting Local Media Files
After uploading and updating references, we delete the local media files:
1asyncfunctiondeleteLocalFile(filepath:string){2try{3await fs.unlink(filepath);4console.log(`Deleted local file: ${filepath}`);5}catch(error){6console.error(`Error deleting local file ${filepath}:`, error);7}8}
typescript
This function is called after successfully updating the references in the Markdown files.
Step 5 - Putting Together the Full Script
The main function orchestrates the entire process:
1constmain=async()=>{2const contentDirectory ="./blog";// Adjust this to your content directory3awaitfindLocalMediaReferences(contentDirectory)4.then((files)=>5 files.forEach(async({ fullPath, destinationDirName })=>{6const status =awaituploadFile({7 filepath: fullPath,8 destinationDirName,9});10replaceLocalReferenceWithRemote(status);11})12)13.catch((error)=>console.error("Error finding local media files:", error));14};
typescript
This function ties together all the previous steps: finding local media references, uploading files, replacing references, and deleting local files.
Step 6 - Orchestrating the Script with a Pre-commit Hook
To run this script automatically before each commit, you can set up a pre-commit hook in your Git repository. Create a file named pre-commit in the .git/hooks/ directory of your repository with the following content:
1#!/bin/bash23# Exit on any error4set -e
56# Run your existing script to replace image references and delete images7bun run upload-media
89# Stage all changes10gitadd -A
1112# Exit successfully13exit0
bash
Make sure to make the hook executable:
chmod +x .git/hooks/pre-commit
Now, every time I attempt to make a commit, this script will run automatically, ensuring that all media files are uploaded to Supabase and references are updated before the commit is made.
And Tadaaaaa - right now this image is a local reference on my computer, but will be sent to Supabase and referenced there as soon as I commit my changes to git.