CTEC2712 Web application development

Working with files

Multimedia in the browser and server

Contents

  1. Uploading files
  2. Validating files
  3. Storing files in SQLite
  4. Retrieving files
  5. Serving files
  6. Rendering images

Uploading files

Uploading files requires a few tweaks to our forms.

multipart/form-data encoding` handles the binary content of the file.

1
2
3
<form enctype="multipart/form-data">

</form>

Files will be ignored unless the form makes a POST request.

1
2
3
<form method="POST" action="/images" enctype="multipart/form-data">

</form>

Use an input element with type=“file” to open a file browser to locate a file.

1
2
3
4
<form action="/images" enctype="multipart/form-data">
    <label for="image">File:</input>
    <input id="image" type="file" name="image">
</form>

We can restrict which file types are allowed with the accept attribute.

1
2
3
4
<form method="POST" action="/images" enctype="multipart/form-data">
    <label for="image">File:</input>
    <input id="image" type="file" name="image" accept="image/*">
</form>

With these in place, the user can pick a file to attach to the request.

1

Validating files

The formData will include a file object which can be extracted in the normal way.

A simple validation function.

1
2
3
4
export function isImageFile(name, value) { 
    if (!(value instanceof File)) return `${name} must be a file.`;
    if (!value.type.startsWith('image/')) return `${name} must be an image file.`;
}

Schema for a simple file upload.

1
2
3
4
export const imageUploadSchema = { 
    title: [required, minLength(6)],
    file: [required, isImageFile]
}

Grab the validated file and pass it to a model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async function uploadController({ request }) {
    const formData = await request.formData();
    const { isValid, errors, validated } = validateSchema(formData, newImageSchema);
    if (isValid) {
        createImage(validated)
        return redirect('/');
    } else {
        return uploadFormController({errors});
    }    
}

Handling the file is just the same as a string, except it is a File object.

2

Storing files in SQLite

Storing files in the database requires some simple manipulation of the file object.

We can store the name and type as text and add a BLOB field for the data.

Other tables can have associated files.

1
2
3
4
5
6
CREATE TABLE files (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    type TEXT NOT NULL,
    bytes BLOB NOT NULL
);
1
2
3
4
5
6
CREATE TABLE images (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    fileId INTEGER NOT NULL,
    FOREIGN KEY (fileId) REFERENCES files(id)
);

We deconstruct the File object. The bytes function gives us our BLOB data.

The image table has a random UUID for the URL.

1
2
3
4
5
6
7
8
9
export async function createFile(file) {
    const { name, type } = file;
    const bytes = await file.bytes();
    return db.prepare(`
        INSERT INTO files(name, type, bytes)
        VALUES (:name, :type, :bytes )
        RETURNING id;
    `).get({ name, type, bytes });
}
1
2
3
4
5
6
7
8
export async function createImage({title, file}) {
    const id = crypto.randomUUID();
    const fileRecord = await createFile(file);
    db.prepare(`
        INSERT INTO images (id, title, fileId)
        VALUES (:id, :title, :fileId)
    `).run({id, title, fileId: fileRecord.id});
}

You may want to have more specific tables with more fields.

3

Retrieving files

Our model rebuilds the file.

Getting data back out of the database requires that we reconstruct the file object.

1
2
3
4
5
6
export function getFile(fileId) {    
    const { type, name, bytes } = db.prepare(
        `SELECT * FROM files WHERE id=:fileId`
    ).get(fileId);
    return new File([bytes], name, { type });
}

The image table is standard

1
2
3
4
5
6
7
export function getImages() {
    return db.prepare(`SELECT * FROM images;`).all();
}

export function getImage(imageId) {
    return db.prepare(`SELECT * FROM images WHERE id=:imageId;`).get({ imageId });
}

The model acts as a simple file store.

4

Serving files

Images are served as image files.

The response object is clever enough to add the correct headers.

1
2
3
4
export function imageController({ imageId }) {
    const file = getFile(imageId);
    return new Response(file);
}

We can route requests to the controller to give each file a URL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const uploadPattern = new URLPattern({pathname: "/"})
const imagePattern = new URLPattern({pathname: "/images/:imageId"})

function server(request) { 
    const url = new URL(request.url);
    const { method } = request;

    if (uploadPattern.test(url) && method == "GET") return uploadFormController({});
    if (uploadPattern.test(url) && method == "POST") return uploadController({request});
    if (imagePattern.test(url) && method == "GET") {
        const { imageId } = imagePattern.exec(url).pathname.groups;        
        return imageController({imageId});
    }
}

Each individual image record has a unique route.

5

Rendering images

We can render images using <img> elements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export function uploadView({ images, errors = {} }) { 
    const imageElements = images.map((i) => `
    <figure>
        <img src="/images/${i.id}" style="width: 100%">
        <figcaption style="padding: 0.5rem;">${i.title}</figcaption>
    </figure>`);
    return `
    <section>
        ${imageElements.join("\n")}
    </section>
    <form action="/" method="POST" enctype="multipart/form-data">
        <label for="image">Image file: </label>
        <input type="file" id="image" accept="image/*" name="file">
        <span class="error">${errors.file?.message || ""}</span>
        <label for="title">title: </label>
        <input type="text" id="title" name="title" value="${errors.title?.value || ""}">
        <span class="error">${errors.title?.message || ""}</span>
        <button>Upload</button>
    </form>`    
}

Setting the src attribute to the appropriate url.

6

Working with files

If you have any questions, now is a good time to ask.

Thanks for listening