Published: Mar 19, 2024 by Isaac Johnson
Recently, I posted about creating upload webapps and thought it might be fun to make my own. In this post, we’ll start with ReactJS and see where that takes us. Like before, the goal is to build something that can allow an end user to upload a file and then for us to be able to retrieve it later.
Build our own
Let’s whip up a quick ReactJS app.
I’ll use NodeJS 18 for this
builder@DESKTOP-QADGF36:~/Workspaces/reactUploader$ nvm use 18.18.2
Now using node v18.18.2 (npm v9.8.1)
I first started down the path of ReactJS.
builder@DESKTOP-QADGF36:~/Workspaces/reactUploader$ npx create-react-app webuploadtool
Creating a new React app in /home/builder/Workspaces/reactUploader/webuploadtool.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...
added 1488 packages in 37s
253 packages are looking for funding
run `npm fund` for details
Initialized a git repository.
Installing template dependencies using npm...
added 69 packages, and changed 1 package in 6s
257 packages are looking for funding
run `npm fund` for details
Removing template package using npm...
removed 1 package, and audited 1557 packages in 2s
257 packages are looking for funding
run `npm fund` for details
8 vulnerabilities (2 moderate, 6 high)
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
Created git commit.
Success! Created webuploadtool at /home/builder/Workspaces/reactUploader/webuploadtool
Inside that directory, you can run several commands:
npm start
Starts the development server.
npm run build
Bundles the app into static files for production.
npm test
Starts the test runner.
npm run eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd webuploadtool
npm start
Happy hacking!
However, after building out a decide File upload JS, I realized there just would not be a way in React to receive the fileContents in Docker without adding Express or some other API server.
import React, { useState } from 'react';
const FileUpload = () => {
const [file, setFile] = useState(null);
const [fileName, setFileName] = useState(null);
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
setFile(selectedFile);
setFileName(selectedFile.name);
};
const handleUpload = () => {
if (!file) {
alert('Please select a file first.');
return;
}
const reader = new FileReader();
reader.onload = () => {
const fileContent = reader.result;
// Code to upload the file content to /tmp on the container
// This can vary depending on your backend/server setup.
// You might need to make a POST request to your server to handle the file upload.
// Example:
// fetch('/upload', {
// method: 'POST',
// body: fileContent,
// });
console.log('File content:', fileContent);
fetch('/uploadrec', {
method: 'POST',
body: fileContent,
});
};
reader.readAsText(file);
};
return (
<div>
<h2>File Uploader</h2>
<input type="file" onChange={handleFileChange} />
<button onClick={handleUpload}>Upload</button>
</div>
);
};
export default FileUpload;
That is a lot of code (and I’m just showing the fileUpload.js above skipping App.js and the rest) just for a UI.
Pivot to ExpressJS
I decided to stop and pivot to Express. For what I’m doing, it’s a lot simpler and can do serverside work.
$ npm init
I installed the libraries I would need
$ npm install --save express multer path fs
I could then create the server.js
// server.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
// Set up multer to handle file uploads
const upload = multer({ dest: process.env.DESTINATION_PATH || '/tmp' });
// Serve the HTML form
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.get('/simple.min.css', (req, res) => {
res.sendFile(path.join(__dirname, 'simple.min.css'));
});
// Handle file upload
app.post('/upload', upload.single('file'), (req, res) => {
const tempFilePath = req.file.path;
const originalFileName = req.file.originalname;
const destinationFilePath = path.join(process.env.DESTINATION_PATH || '/tmp', originalFileName);
fs.rename(tempFilePath, destinationFilePath, (err) => {
if (err) {
console.error('Error moving file:', err);
res.status(500).send('Error moving file');
} else {
console.log('File saved successfully:', destinationFilePath);
res.status(200).send('File uploaded successfully');
}
});
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Note: I already parameterized the PORT and DESTINATION_PATH knowing I would need to be able to override them
I updated my ‘main’ file to use ‘server.js’ instead of ‘index.js’ in package.json
{
"name": "webuploadtool3",
"version": "1.0.0",
"description": "Upload tool in expressjs",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"express"
],
"author": "Isaac Johnson",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"fs": "^0.0.1-security",
"multer": "^1.4.5-lts.1",
"path": "^0.12.7"
}
}
My index.html is pretty straightforward except for importing the CSS
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/simple.min.css" />
<title>File Upload</title>
</head>
<body>
<h1>Upload a File</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Upload</button>
</form>
</body>
</html>
I downloaded the CSS from https://simplecss.org/
$ wget https://cdn.simplecss.org/simple.min.css
We could have used the HREF in the <HEAD>
block but then we have an unneccessary external dependency
<!-- Minified version -->
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
Let’s do a quick test
builder@DESKTOP-QADGF36:~/Workspaces/reactUploader/webuploadtool3$ npm start
> webuploadtool3@1.0.0 start
> node server.js
Server is running on port 3000
I’ll pick an image
Click upload
and see in the server log
builder@DESKTOP-QADGF36:~/Workspaces/reactUploader/webuploadtool3$ npm start
> webuploadtool3@1.0.0 start
> node server.js
Server is running on port 3000
File saved successfully: /tmp/downtown_berlin_corperate_office_building_clean_and_bright_2_78710595-fe99-4b58-984d-54eb5e32851d.png
I can kill the server and check
builder@DESKTOP-QADGF36:~/Workspaces/reactUploader/webuploadtool3$ ls -ltrah /tmp/downtown_berlin_corperate_office_building_clean_and_bright_2_78710595-fe99-4b58-984d-54eb5e32851d.png
-rw-r--r-- 1 builder builder 1.6M Feb 27 07:26 /tmp/downtown_berlin_corperate_office_building_clean_and_bright_2_78710595-fe99-4b58-984d-54eb5e32851d.png
I can also try setting a different root path and testing the same file upload:
builder@DESKTOP-QADGF36:~/Workspaces/reactUploader/webuploadtool3$ DESTINATION_PATH=/home/builder/Workspaces/reactUploader/webuploadtool3 npm start
> webuploadtool3@1.0.0 start
> node server.js
Server is running on port 3000
File saved successfully: /home/builder/Workspaces/reactUploader/webuploadtool3/downtown_berlin_corperate_office_building_clean_and_bright_2_78710595-fe99-4b58-984d-54eb5e32851d.png
I’ll next create a Dockerfile. I’m already thinking ahead of publishing the Container on ghcr.io
so I’ll pack in my lable
FROM node:18
# For GHCR
LABEL org.opencontainers.image.source="https://github.com/idjohnson/expressUploader"
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
I can now build out a Github workflow:
$ cat .github/workflows/build.yml
name: Build and Push Docker Image to GHCR
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push Docker image
env:
DOCKER_USERNAME: idjohnson
DOCKER_REGISTRY: ghcr.io
IMAGE_NAME: expressupload
run: |
docker buildx create --use
docker buildx build --push --tag $DOCKER_REGISTRY/$DOCKER_USERNAME/$IMAGE_NAME:latest .
To be clear here, I am not actually setting that secret. This is provided by Github meaning we can build and push without needing to store our GH PAT.
Pushing it creates a new build
My first attempt got an error
However, I pivoted away from buildx and the buildx action to:
name: Build and Push Docker Image to GHCR
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login to GitHub Container Registry'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: $
password: $
- name: 'Build and Push Image'
run: |
docker build . --tag ghcr.io/$/expressupload:latest
docker push ghcr.io/$/expressupload:latest
This worked
Now I see a package under packages:
Here we can see the pull command
GHCR is not anonymous, so we still need to login to use it (free, but you’ll have to let Github know who you are):
$ export CR_PAT=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
$ echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
Login Succeeded
$ docker run -p 3333:3000 ghcr.io/idjohnson/expressupload:latest
Unable to find image 'ghcr.io/idjohnson/expressupload:latest' locally
latest: Pulling from idjohnson/expressupload
7bb465c29149: Pull complete
2b9b41aaa3c5: Pull complete
49b40be4436e: Pull complete
c558fac597f8: Pull complete
449619e06fe3: Pull complete
f52e55fee245: Pull complete
b51b5379e841: Pull complete
806ff1e3aade: Pull complete
0be895befe98: Pull complete
373220c47dd3: Pull complete
d554e55bb6e5: Pull complete
c479181ed4b3: Pull complete
Digest: sha256:89d948ce37a5cd2e2508889d1a157be07b201767a810b8a706aecab91f1c49f1
Status: Downloaded newer image for ghcr.io/idjohnson/expressupload:latest
> webuploadtool3@1.0.0 start
> node server.js
Server is running on port 3000
I can see it working
Then testing an upload
I see in the output
> webuploadtool3@1.0.0 start
> node server.js
Server is running on port 3000
File saved successfully: /tmp/downtown_berlin_corperate_office_building_clean_and_bright_2_78710595-fe99-4b58-984d-54eb5e32851d.png
And lastly, confirmation the file lives in the Docker container
Kubernetes
Following this writeup we need to create a Kubernetes image pull secret to use GHCR.io. There is a nice guide here on how to make an Image Pull Secret.
For instance, if our Github username and PAT looks like ‘username:123123adsfasdf123123’, we can base64 that value:
$ echo username:123123adsfasdf123123 | tr -d '\n' | base64
dXNlcm5hbWU6MTIzMTIzYWRzZmFzZGYxMjMxMjM
And to turn that into a base64 auth value for our secrets, we can use:
$ echo '{"auths":{"ghcr.io":{"auth":"dXNlcm5hbWU6MTIzMTIzYWRzZmFzZGYxMjMxMjM="}}}' | tr -d '\n' | base64 -w 0
eyJhdXRocyI6eyJnaGNyLmlvIjp7ImF1dGgiOiJkWE5sY201aGJXVTZNVEl6TVRJellXUnpabUZ6WkdZeE1qTXhNak09In19fQ==
I’ll feed that to a secret
$ kubectl get secrets dockerconfigjson-github-com -o yaml
apiVersion: v1
kind: Secret
data:
.dockerconfigjson: eyJhdXRocyI6eyJnaGNyLmlvIjp7ImF1dGgiOiJkWE5sY201aGJXVTZNVEl6TVRJellXUnpabUZ6WkdZeE1qTXhNak09In19fQ==
metadata:
name: dockerconfigjson-github-com
I can then use with a pod to test
apiVersion: v1
kind: Pod
metadata:
name: name
spec:
containers:
- name: name
image: ghcr.io/username/imagename:label
imagePullPolicy: Always
imagePullSecrets:
- name: dockerconfigjson-github-com
I need to test while away, so I’ll build and push to dockerhub locally
builder@LuiGi17:~/Workspaces/expressUploader$ docker build -t idjohnson/expressuploader:latest .
[+] Building 108.5s (11/11) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.2s
=> => transferring dockerfile: 254B 0.0s
=> [internal] load .dockerignore 0.2s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:18 2.4s
=> [auth] library/node:pull token for registry-1.docker.io 0.0s
=> [internal] load build context 0.2s
=> => transferring context: 95.69kB 0.1s
=> [1/5] FROM docker.io/library/node:18@sha256:aa329c613f0067755c0787d2a3a9802c7d95eecdb927d62b910ec1d28689882f 101.4s
=> => resolve docker.io/library/node:18@sha256:aa329c613f0067755c0787d2a3a9802c7d95eecdb927d62b910ec1d28689882f 0.1s
=> => sha256:aa329c613f0067755c0787d2a3a9802c7d95eecdb927d62b910ec1d28689882f 1.21kB / 1.21kB 0.0s
=> => sha256:256406951729eec0e9f15266bf77ec0b07260c2087cba2f5ca98951c099c0154 2.00kB / 2.00kB 0.0s
=> => sha256:39e94893115b118c1ada01fbdd42e954bb70f847933e91c3bcda278c96ad4ca2 7.34kB / 7.34kB 0.0s
=> => sha256:2b9b41aaa3c52ab268b47da303015b94ced04a1eb02e58860e58b283404974f4 24.05MB / 24.05MB 4.4s
=> => sha256:7bb465c2914923b08ae03b7fc67b92a1ef9b09c4c1eb9d6711b22ee6bbb46a00 49.55MB / 49.55MB 21.7s
=> => sha256:49b40be4436eff6fe463f6977159dc727df37cabe65ade75c75c1caa3cb0a234 64.14MB / 64.14MB 28.7s
=> => sha256:c558fac597f8ecbb7a66712e14912ce1d83b23a92ca8b6ff14eef209ab01aff2 211.12MB / 211.12MB 67.5s
=> => sha256:449619e06fe37ae34d5d3e31b928b174f8b0eee07dfca3669c4a8e6326b8e82c 3.37kB / 3.37kB 21.9s
=> => sha256:f52e55fee2455f6af71ffa5cfbdbb01603063f889fff52c2fd74df2673f07500 46.03MB / 46.03MB 38.6s
=> => extracting sha256:7bb465c2914923b08ae03b7fc67b92a1ef9b09c4c1eb9d6711b22ee6bbb46a00 9.9s
=> => sha256:b51b5379e841a41fe00a85aaf18cc0ab927c2d42ef40a9cc3211df2e2cddaf25 2.21MB / 2.21MB 30.2s
=> => sha256:806ff1e3aadedd43a235140404bf6ff4b790d4dfd56c93a7dfb3cc78e50ebca5 450B / 450B 30.4s
=> => extracting sha256:2b9b41aaa3c52ab268b47da303015b94ced04a1eb02e58860e58b283404974f4 2.3s
=> => extracting sha256:49b40be4436eff6fe463f6977159dc727df37cabe65ade75c75c1caa3cb0a234 9.6s
=> => extracting sha256:c558fac597f8ecbb7a66712e14912ce1d83b23a92ca8b6ff14eef209ab01aff2 25.5s
=> => extracting sha256:449619e06fe37ae34d5d3e31b928b174f8b0eee07dfca3669c4a8e6326b8e82c 0.0s
=> => extracting sha256:f52e55fee2455f6af71ffa5cfbdbb01603063f889fff52c2fd74df2673f07500 6.8s
=> => extracting sha256:b51b5379e841a41fe00a85aaf18cc0ab927c2d42ef40a9cc3211df2e2cddaf25 0.2s
=> => extracting sha256:806ff1e3aadedd43a235140404bf6ff4b790d4dfd56c93a7dfb3cc78e50ebca5 0.0s
=> [2/5] WORKDIR /usr/src/app 0.3s
=> [3/5] COPY package*.json ./ 0.1s
=> [4/5] RUN npm install 3.2s
=> [5/5] COPY . . 0.2s
=> exporting to image 0.3s
=> => exporting layers 0.2s
=> => writing image sha256:8a0601b12a964ff8e7e77a50d719b75313c451d6a1300577829c794a1288ac7a 0.0s
=> => naming to docker.io/idjohnson/expressuploader:latest 0.1s
What's Next?
View a summary of image vulnerabilities and recommendations → docker scout quickview
builder@LuiGi17:~/Workspaces/expressUploader$ docker push idjohnson/expressuploader:latest
The push refers to repository [docker.io/idjohnson/expressuploader]
0a51eea14f9e: Pushed
e437af4a5038: Pushed
6832492f9d34: Pushed
0b14f953e13b: Pushed
ba70e4e5685e: Mounted from library/node
5fae8bf6e262: Mounted from library/node
2793982da9d3: Mounted from library/node
2c2decbeb47f: Mounted from library/node
9fe4e8a1862c: Mounted from library/node
909275a3eaaa: Mounted from library/node
f3f47b3309ca: Mounted from library/node
1a5fc1184c48: Mounted from library/node
latest: digest: sha256:c5a6e0e260c33ae7322fd1e4942e6b10d0d2a10baad6d4c79cee6e8a17a6d32a size: 2840
Here we can create a YAML
builder@LuiGi17:~/Workspaces/expressUploader/yaml$ cat manifest.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: expressuploader-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: expressuploader
spec:
replicas: 1
selector:
matchLabels:
app: expressuploader
template:
metadata:
labels:
app: expressuploader
spec:
containers:
- name: expressuploader
image: idjohnson/expressuploader:latest
ports:
- containerPort: 3000
env:
- name: DESTINATION_PATH
value: "/mnt/storage"
volumeMounts:
- name: expressuploader-storage
mountPath: /mnt/storage
volumes:
- name: expressuploader-storage
persistentVolumeClaim:
claimName: expressuploader-pvc
---
apiVersion: v1
kind: Service
metadata:
name: expressuploader-service
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 3000
selector:
app: expressuploader
which we can try
$ kubectl apply -f ./manifest.yaml
persistentvolumeclaim/expressuploader-pvc created
deployment.apps/expressuploader created
service/expressuploader-service created
I can quick check and see they all exist
builder@LuiGi17:~/Workspaces/expressUploader/yaml$ kubectl get pods | grep express
expressuploader-8c86455b6-xgcgs 1/1 Running 0 3m23s
builder@LuiGi17:~/Workspaces/expressUploader/yaml$ kubectl get pvc | grep express
expressuploader-pvc Bound pvc-a86b2353-c73c-4f2e-b795-9de7b5fedd23 5Gi RWO local-path 4m2s
builder@LuiGi17:~/Workspaces/expressUploader/yaml$ kubectl get svc | grep express
expressuploader-service ClusterIP 10.43.117.80 <none> 80/TCP 4m10s
Helm
Manifests are good, but so is good old fashioned Helm.
Let’s turn that YAML into a chart.
I’ll first delete my former
$ kubectl delete -f ./yaml/manifest.yaml
persistentvolumeclaim "expressuploader-pvc" deleted
deployment.apps "expressuploader" deleted
service "expressuploader-service" deleted
I’ll build out the files as such
builder@LuiGi17:~/Workspaces/expressUploader$ tree ./helm/
./helm/
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── pvc.yaml
│ └── service.yaml
└── values.yaml
1 directory, 5 files
The Chart and values
builder@LuiGi17:~/Workspaces/expressUploader$ cat helm/Chart.yaml
apiVersion: v2
name: expressupload
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
builder@LuiGi17:~/Workspaces/expressUploader$ cat helm/values.yaml
replicaCount: 1
image:
repository: idjohnson/expressuploader
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
pvc:
accessModes:
- ReadWriteOnce
storage: 5Gi
storageClassName: "local-path"
In the deployment YAML we can see how we set the env var to the upload path serviced by the PVC
$ cat helm/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: expressuploader
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: expressuploader
template:
metadata:
labels:
app: expressuploader
spec:
containers:
- name: expressuploader
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 3000
env:
- name: DESTINATION_PATH
value: "/mnt/storage"
volumeMounts:
- name: expressuploader-storage
mountPath: /mnt/storage
volumes:
- name: expressuploader-storage
persistentVolumeClaim:
claimName: expressuploader-pvc
The service will let us specify a port, but we’ve left the target port hardcoded to 3000
$ cat helm/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: expressuploader-service
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: 3000
selector:
app: expressuploader
Lastly, the PVC
r$ cat helm/templates/pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: expressuploader-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.pvc.storage }}
storageClassName: {{ .Values.pvc.storageClassName }}
I can test an install with helm locally
$ helm install myexpressupload ./helm/
NAME: myexpressupload
LAST DEPLOYED: Tue Feb 27 18:36:21 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
Which also worked great
builder@LuiGi17:~/Workspaces/expressUploader$ kubectl get pvc expressuploader-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
expressuploader-pvc Bound pvc-300252d1-a227-431c-ac2b-5b34bd57def6 5Gi RWO local-path 2m26s
builder@LuiGi17:~/Workspaces/expressUploader$ kubectl get pods | grep express
expressuploader-5f5768445b-k2k87 1/1 Running 0 2m22s
builder@LuiGi17:~/Workspaces/expressUploader$ kubectl get svc | grep express
expressuploader-service ClusterIP 10.43.65.220 <none> 80/TCP 2m43s
My last step is to see if Github Workflows can use GHCR.io as an OCI helm repository.
I added
- name: 'Helm package and push'
run: |
helm package ./helm/
export HELM_EXPERIMENTAL_OCI=1
helm push expressupload-0.1.0.tgz oci://ghcr.io/$/
Then I added all this and pushed to Github
builder@LuiGi17:~/Workspaces/expressUploader$ git add helm/
builder@LuiGi17:~/Workspaces/expressUploader$ git add yaml/
builder@LuiGi17:~/Workspaces/expressUploader$ git add .github/workflows/build.yml
builder@LuiGi17:~/Workspaces/expressUploader$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: .github/workflows/build.yml
new file: helm/Chart.yaml
new file: helm/templates/deployment.yaml
new file: helm/templates/pvc.yaml
new file: helm/templates/service.yaml
new file: helm/values.yaml
new file: yaml/manifest.yaml
builder@LuiGi17:~/Workspaces/expressUploader$ git commit -m "Add YAML, Helm, and a build step to package and publish helm"
[main 08bf91b] Add YAML, Helm, and a build step to package and publish helm
7 files changed, 130 insertions(+), 1 deletion(-)
create mode 100644 helm/Chart.yaml
create mode 100644 helm/templates/deployment.yaml
create mode 100644 helm/templates/pvc.yaml
create mode 100644 helm/templates/service.yaml
create mode 100644 helm/values.yaml
create mode 100644 yaml/manifest.yaml
builder@LuiGi17:~/Workspaces/expressUploader$ git push
Enumerating objects: 18, done.
Counting objects: 100% (18/18), done.
Delta compression using up to 16 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (14/14), 1.87 KiB | 478.00 KiB/s, done.
Total 14 (delta 4), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (4/4), completed with 2 local objects.
To https://github.com/idjohnson/expressUploader.git
bfe81bb..08bf91b main -> main
which worked
Which I can now use locally without having to have the repo cloned down
$ helm install myexpress2 oci://ghcr.io/idjohnson/expressupload --version 0.1.0
Pulled: ghcr.io/idjohnson/expressupload:0.1.0
Digest: sha256:8c6bc99b796134285269d12e23ae905d6dc9fb513475dc253b346f900db559e1
NAME: myexpress2
LAST DEPLOYED: Tue Feb 27 18:49:59 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
One last quick change to avoid that hard-coded version number
builder@LuiGi17:~/Workspaces/expressUploader$ git diff .github/workflows/build.yml
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2e37cd5..7d6d773 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -31,4 +31,4 @@ jobs:
run: |
helm package ./helm/
export HELM_EXPERIMENTAL_OCI=1
- helm push expressupload-0.1.0.tgz oci://ghcr.io/$/
\ No newline at end of file
+ helm push expressupload-*.tgz oci://ghcr.io/$/
\ No newline at end of file
builder@LuiGi17:~/Workspaces/expressUploader$ git add .github/workflows/build.yml
builder@LuiGi17:~/Workspaces/expressUploader$ git commit -m "remove hardcoded version for future use"
[main 70b7270] remove hardcoded version for future use
1 file changed, 1 insertion(+), 1 deletion(-)
builder@LuiGi17:~/Workspaces/expressUploader$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 413 bytes | 413.00 KiB/s, done.
Total 5 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/idjohnson/expressUploader.git
08bf91b..70b7270 main -> main
The very last thing I’ll do before wrapping is to create a README.md
I had the need for a simple containerized uploader. Whether you are looking to let students upload assignments, or contractors their timecards or maybe you are running a contest or a tech conference seeking RFPs.
The files are stored in a configurable path that can be mounted to a docker volume or host path, or used in Kubernetes in a PVC.
You can set DESTINATION_PATH at app invokation
$ DESTINATION_PATH=/tmp/mypath npm start
Or with docker run
$ docker run -p 8888:3000 -e DESTINATION_PATH=/tmp/mypath ghcr.io/idjohnson/expressupload:latest
It is not exposed with helm because the destination maps to a PVC which is configuration on sizes
$ helm install myexpress2 --set pvc.storage=10Gi oci://ghcr.io/idjohnson/expressupload --version 0.1.0
Summary
I hope you enjoyed this quick writeup on how to create and use an ExpressJS based containerized web uploader. There is certainly lots we can do to extend this and refine it. However, the goal here was to just walk through a simple start to finish app