labnotes

Image upload with node storing on a seperate directory

why do anything so fancy as S3

tags
flyio
vite
javascript
1
  npm i express express-fileupload vite cors

This will give us something like

package.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  {
      "type": "module",
      "scripts": {
          "dev": "node app.js & vite",
          "build": "vite build"
      },
      "optionalDependencies": {
          "@rollup/rollup-linux-x64-gnu": "4.6.1"
      },
      "dependencies": {
          "cors": "^2.8.5",
          "express": "^4.19.2",
          "express-fileupload": "^1.5.0",
          "vite": "^5.2.8"
      }
  }

Server App

Our first bit of code is going to deal with moving the files around. express-fileupload will get the file and put it in a temp directory, and this will handle moving the code into our data directory. If DATA_DIR is in the enviroment it will put it there, otherwise it will stick it in a local uploads/ folder.

file_storage.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  import * as fs from 'fs';
  import path from 'node:path';
  import process from 'node:process';

  export const dir = process.env.DATA_DIR ? process.env.DATA_DIR :
      path.normalize( path.join( process.cwd(), 'upload' ));

  try {
      if (!fs.existsSync(dir)) {
          fs.mkdirSync(dir);
      }
  } catch (err) {
      console.error(err);
  }

  export function entries() {
      const all_files = fs.readdirSync(dir, {withFileTypes: true})
      const files = all_files.filter((dirent) => dirent.isFile()).map((f) => f.name);
      
      return files;
  }

  export function storeImage(image) {
      const count = entries().length
      const ext = path.extname(image.name);
      const name = `${dir}/${count}${ext}`
      
      console.log( "Storing file in ", name );
      
      image.mv( name )
  }

  export function realPath(name) {
      const basename = path.basename(name);
      return path.normalize(`${dir}/${basename}`)
  }

  console.log( entries() )

Now to the app itself. We'll serve our javascript application out of dist/ which is where vite will eventually build it, create an /images GET route to return a list of images that we have in our folder, an /images/:name route which will return the image, and an /upload POST route to accept the uploaded file and store it in the directory.

app.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
  import express from 'express'
  import fileUpload from 'express-fileupload'
  import { storeImage, realPath, entries } from './file_storage.js'
  import cors from 'cors';

  const app = express();
  //if( import.meta.env.MODE == 'development' ) {
      app.use(cors())
  //}

  app.use(fileUpload());

  const port = 3000;

  app.use(express.static('dist/'));

  app.get('/', (req, res) => {
      res.send('Hello World!');
  });

  app.get( '/images', (req, res) => {
      const list = entries().map( (elem) => `/images/${elem}` ) 
      const ret = {
          entries: list
      }

      res.json( ret )
  })

  app.get( '/images/:path', (req, res) => {
      res.sendFile( realPath(req.params.path) );
  } )


  app.post('/upload', (req, res) => {
      // Get the file that was set to our field named "image"
      const { image } = req.files;

      // If no image submitted, exit
      if (!image) return res.sendStatus(400);

      storeImage( image );
      // Move the uploaded image to our upload folder
      //image.mv( './upload/' + image.name);

      res.sendStatus(200);
  });

  app.listen(port, () => {
      console.log(`Example app listening on port ${port}`);
  });

Client App

Here we are splitting up the code into two different components, capture-photo and photo-list. Clearly some work could be done on design.

index.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  <html>
    <head>
      <title>Image Uploader Test</title>
      <script src="photo-list.js" type="module"></script>
      <script src="capture-photo.js" type="module"></script>
      <meta name="viewport" content="width=device-width, initial-scale=1" />
    
    </head>
    <body >
      <p>Select a file to upload</p>
      <capture-photo></capture-photo>
      <photo-list></photo-list>
    </body>
  </html>

This makes an input type=file which will prompt the user for photo (or, on mobile, go to the camera itself) and then post the file back to the server. Either the express app running on port 3000 locally or whatever the base url of the deployed site is in production.

After it's done it dispatches a refresh event on the window with the other component can watch for.

capture-photo.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  class CapturePhoto extends HTMLElement {
      connectedCallback() {
          this.innerHTML= `<input type="file" name="selectedPicture" id="selectedPicture" 
       accept="image/*" capture
        />`;
          const input = this.querySelector( "input" );
          input.addEventListener( "change", (e) => {
              this.uploadPhoto();
              console.log( input );
              console.log( e );
          });
      }

      async uploadPhoto() {
          const formData = new FormData();
          const input = this.querySelector( "input" );

          formData.append( "image", input.files[0] );
          input.value = ''
          const host = import.meta.env.MODE == 'development' ? "http://localhost:3000" : ""

          const response = await fetch(`${host}/upload`, {
              method: "POST",
              body: formData,
          });

          const event = new CustomEvent("refresh" )
          window.dispatchEvent( event );

          const result = await response.json();
          console.log( result );
      }
  }

  customElements.define( 'capture-photo', CapturePhoto );

Hit the endpoint, and make the list of images!

photo-list.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  const host = import.meta.env.MODE == 'development' ? "http://localhost:3000" : ""

  class PhotoList extends HTMLElement {
      connectedCallback() {
          this.list = [];
          this.queryList();
          this.render();

          window.addEventListener( "refresh", () => this.queryList() );
      }

      async queryList()
      {
          const response = await fetch( `${host}/images` )
          const json = await response.json()
          this.list = json.entries
          this.render()
      }

      render() {
          let h = "<ul>"

          for( let i = this.list.length-1; i >= 0; i-- ) {
              let img = this.list[i];

              h += `<li><img src="${host}${img}" style="max-width: 300px"></li>`
          }

          h += `</ul>`

          this.innerHTML = h;
      }
  }

  customElements.define( 'photo-list', PhotoList );

Deploy

Lets make sure that this junk doesn't end up in the docker image.

.dockerignore:

1
2
  node_modules/
  upload/

Build the vite app, then run the express app.

Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FROM node:20.12.0-bookworm

WORKDIR /usr/app

COPY package* ./
RUN npm install

COPY . ./
RUN npx vite build

EXPOSE 3000

CMD node app.js

The key parts of this file are the [mounts] section which defines the persistent storage and the [env] section which tells our code where it is.

fly.toml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app = 'wschenk-test'
primary_region = 'ewr'

[build]

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  memory = '512mb'
  cpu_kind = 'shared'
  cpus = 1

[mounts]
  source="myapp_data"
  destination="/data"

[env]
  DATA_DIR="/data"

Previously

labnotes

Vite and express development

javascript all the way down

tags
vite
express
docker
flyio

Next

labnotes

Streaming responses from ollama

really any fetch thing

tags
javascript
ollama