This post won’t share the exact code behind the app, as it is available on GitHub, so if you are interested you can check it out below.

The code for this project is available on the GitHub here

The website for this is hosted here.

What are we using?

In this project, we will use flask and python for the website and CloudFlare R2 for storage.

Installing the requirements

Windows

1
py -m pip install flask python-dotenv boto3

Linux/Mac

1
pip3 install flask python-dotenv boto3

Planning

Below is a simple design I made for the homepage of the app. This will be what I try to make my app look like when I am styling it. Image of a plan for an interface

Initial code

Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from flask import Flask, render_template
from dotenv import load_dotenv
import os

load_dotenv()

R2_S3_LINK = os.getenv("R2_S3_LINK")
R2_ACCESS_KEY_ID = os.getenv("R2_ACCESS_KEY_ID")
R2_SECRET_ACCESS_KEY = os.getenv("R2_SECRET_ACCESS_KEY")

app = Flask(__name__)

@app.route("/")
def index():
	return render_template("index.html")

if __name__ == "__main__":
	app.run(debug=True)

HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Image Host</title>
	</head>
	<body>
		<h1>Image Host</h1>
	</body>
</html>

.env:

1
2
3
R2_S3_LINK=''
R2_ACCESS_KEY_ID=''
R2_SECRET_ACCESS_KEY=''

This code is relatively simple, it should only display a plain page with the text “image host” if you run it and open the webpage. This will be fixed soon.

Next, I will be adding styles, error handling, image uploading, image deletions, and more. This will more or less be the completed app.

Final Code

index.html:

  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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Image Host</title>
		<link
			rel="stylesheet"
			href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css"
		/>
		<link
			rel="stylesheet"
			href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css"
			integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ=="
			crossorigin="anonymous"
			referrerpolicy="no-referrer"
		/>
	</head>
	<body>
		<div class="container mt-6">
			<div class="box">
				<h1 class="title has-text-centered">Image Host</h1>
				<div
					class="has-text-centered is-flex is-justify-content-center"
				>
					<div class="file is-large has-name is-boxed">
						<label class="file-label">
							<input
								class="file-input"
								type="file"
								name="file"
								id="fileupload"
								accept="image/png, image/jpeg, image/webp, image/gif"
							/>
							<span class="file-cta">
								<span class="file-icon">
									<i class="fas fa-upload"></i>
								</span>
								<span class="file-label">Choose a file...</span>
							</span>
							<span class="file-name" id="filename">
								No file selected
							</span>
						</label>
					</div>
				</div>
				<div class="buttons is-centered mt-3">
					<button id="upload" class="button is-success is-large">
						Upload
					</button>
				</div>
			</div>
		</div>
		<footer class="footer">
			<div class="content has-text-centered">
				<p>
					Image Host by <a href="https://dfarkas.uk">David Farkas</a>
				</p>
			</div>
		</footer>
	</body>
	<style>
		.footer {
			position: fixed;
			left: 0;
			bottom: 0;
			width: 100%;
		}
	</style>
	<script>
		document.getElementById("fileupload").onchange = function () {
			document.getElementById("filename").innerHTML = this.files[0].name;
		};
		document.getElementById("upload").onclick = function () {
			document.getElementById("upload").disabled = true;
			document.getElementById("upload").classList.add("is-loading");
			var xhr = new XMLHttpRequest();
			xhr.open("POST", "/upload", true);
			var data = new FormData();
			data.append("file", document.getElementById("fileupload").files[0]);
			xhr.send(data);
			xhr.onload = function () {
				document.getElementById("upload").disabled = false;
				document
					.getElementById("upload")
					.classList.remove("is-loading");
				var response = JSON.parse(xhr.responseText);
				if (response.error) {
					alert(response.error);
				} else {
					window.location.href =
						"/image/" +
						response.uid +
						"." +
						response.extension +
						"/frame" +
						"?deletekey=" +
						response.deletekey;
				}
			};
		};
	</script>
</html>

delete.html

 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
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Delete an Image</title>
		<link
			rel="stylesheet"
			href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css"
		/>
	</head>
	<body>
		<div class="container">
			<div class="box">
				<h1 class="title has-text-centered">Delete an Image</h1>
				<form action="/delete" method="POST">
					<div class="field">
						<label class="label">Image UID</label>
						<div class="control">
							<input
								class="input"
								type="text"
								name="uid"
								placeholder="Enter the UID of the image you want to delete"
								id="uid"
								required
							/>
						</div>
					</div>
					<div class="field">
						<label class="label">Image Delete Key</label>
						<div class="control">
							<input
								class="input"
								type="text"
								name="deletekey"
								placeholder="Enter the delete key of the image you want to delete"
								id="deletekey"
								required
							/>
						</div>
					</div>
					<div class="field is-grouped is-grouped-centered">
						<div class="control">
							<button class="button is-danger">Delete</button>
						</div>
					</div>
				</form>
			</div>
		</div>
	</body>
	<script>
		document.getElementById("uid").focus();
		// get query parameters
		const urlParams = new URLSearchParams(window.location.search);
		const uid = urlParams.get("uid");
		const deletekey = urlParams.get("deletekey");
		// set form values
		document.getElementById("uid").value = uid;
		document.getElementById("deletekey").value = deletekey;
	</script>
</html>

error.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Error</title>
		<link
			rel="stylesheet"
			href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css"
		/>
	</head>
	<body>
		<div class="container mt-6">
			<div class="box">
				<h1 class="title has-text-centered">Error</h1>
				<p class="has-text-centered">{{ error }}</p>
			</div>
		</div>
	</body>
</html>

frame.html

 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
52
53
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Image Viewer</title>
		<link
			rel="stylesheet"
			href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css"
		/>
	</head>
	<body>
		<div class="container">
			<div class="box">
				<h1 class="title has-text-centered">Image Viewer</h1>
				<div
					class="has-text-centered is-flex is-justify-content-center"
				>
					<img src="/image/{{ filename }}" alt="Image" />
				</div>
				<p class="has-text-centered my-2">
					Delete key: <code>{{ deletekey }}</code>
				</p>
				<p class="has-text-centered">
					Without the delete key, you cannot delete the image.
				</p>
				<a
					href="/delete?uid={{ filename.split('.')[0] }}&deletekey={{ deletekey }}"
					class="button my-2 is-danger is-fullwidth"
					>Delete</a
				>
				<button class="button is-success is-fullwidth" id="copy">
					Copy URL
				</button>
			</div>
		</div>
	</body>
	<style>
		img {
			max-width: 100%;
			max-height: 100%;
			object-fit: contain;
			border: 1px solid black;
			border-radius: 10px;
		}
	</style>
	<script>
		document.getElementById("copy").addEventListener("click", () => {
			const url = `https://image.dfarkas.uk/image/{{ filename }}`;
			navigator.clipboard.writeText(url);
		});
	</script>
</html>

main.py

  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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import time
from flask import Flask, render_template, request, jsonify, send_file
from dotenv import load_dotenv
import os, boto3, io
from botocore.exceptions import NoCredentialsError, PartialCredentialsError
from uuid import uuid4


load_dotenv()

R2_S3_LINK = os.getenv("R2_S3_LINK")
R2_ACCESS_KEY_ID = os.getenv("R2_ACCESS_KEY_ID")
R2_SECRET_ACCESS_KEY = os.getenv("R2_SECRET_ACCESS_KEY")
MAX_FILE_SIZE_MB = 3 * 1024 * 1024

s3_client = boto3.client(
    "s3",
    endpoint_url=R2_S3_LINK,
    aws_access_key_id=R2_ACCESS_KEY_ID,
    aws_secret_access_key=R2_SECRET_ACCESS_KEY,
)

app = Flask(__name__)


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/upload", methods=["POST"])
def upload():
    file = request.files["file"]
    # Upload the file to R2
    if file.filename == "":
        return jsonify({"error": "No file selected"}), 400
    # Check file size
    file.seek(0, 2)  # Move the cursor to the end of the file
    file_size = file.tell()  # Get the cursor position, which is the file size
    file.seek(0)  # Reset cursor to the beginning of the file for upload
    if file_size > MAX_FILE_SIZE_MB:
        return jsonify({"error": "File size exceeds the 3MB limit"}), 400
    deletekey = str(uuid4())
    metadata = {
        "filename": file.filename,
        "deletekey": deletekey,
    }
    filename = file.filename
    extension = filename.split(".")[-1]
    uid = str(uuid4())
    try:
        # Upload the file to R2
        s3_client.upload_fileobj(
            file, "image-host", f"{uid}.{extension}", ExtraArgs={"Metadata": metadata}
        )
        return (
            jsonify(
                {
                    "message": "File uploaded successfully",
                    "uid": uid,
                    "extension": extension,
                    "deletekey": deletekey,
                }
            ),
            200,
        )

    except (NoCredentialsError, PartialCredentialsError) as e:
        return render_template("error.html", error=str(e)), 500

    except Exception as e:
        return render_template("error.html", error=str(e)), 500


@app.route("/image/<path:filename>")
def serve_image(filename):
    # Serve the image from R2
    try:
        response = s3_client.get_object(Bucket="image-host", Key=filename)
        return send_file(
            io.BytesIO(response["Body"].read()),
            mimetype="image/jpeg",
            as_attachment=False,
            download_name=f"{filename}",
        )
    except Exception as e:
        return render_template("error.html", error=str(e)), 500


@app.route("/image/<path:filename>/frame")
def serve_frame(filename):
    deletekey = request.args.get("deletekey")
    try:
        s3_client.get_object(Bucket="image-host", Key=filename)
        return render_template("frame.html", filename=filename, deletekey=deletekey)
    except Exception as e:
        return render_template("error.html", error=f"File not found: {filename}"), 500


@app.route("/delete", methods=["GET", "POST"])
def delete():
    if request.method == "GET":
        return render_template("delete.html")
    elif request.method == "POST":
        uid = request.form.get("uid")
        deletekey = request.form.get("deletekey")
        try:
            # find the file name from the uuid prefix
            res = s3_client.list_objects(Bucket="image-host", Prefix=uid)
            if "Contents" not in res:
                return render_template("error.html", error="File not found"), 404
            # set filename
            filename = res["Contents"][0]["Key"].split("/")[-1]
            # find the image using the uid and make sure the deletekey matches
            response = s3_client.head_object(Bucket="image-host", Key=filename)
            metadata = response.get("Metadata", {})
            if metadata.get("deletekey") == deletekey:  # Metadata keys are lowercase
                # Delete the object
                s3_client.delete_object(Bucket="image-host", Key=filename)
                return (
                    render_template("error.html", error="File deleted successfully"),
                    200,
                )
            else:
                # If deleteKey doesn't match, wait 1 second and return an error
                time.sleep(1)
                return (
                    render_template("error.html", error="Delete key does not match"),
                    400,
                )
        except Exception as e:
            return render_template("error.html", error=str(e)), 500


if __name__ == "__main__":
    app.run(debug=False, host="0.0.0.0", port=5132)

How to run

Once you have changed .env.example to .env, and you have filled in the credentials, you can run it as such:

Windows

1
py -3 main.py

Linux/Mac

1
python3 main.py

Conclusion

This app is designed for allowing users to upload a file, and get a link they can then share to other platforms, this achieves that and is relatively simple to use.

Contributing

Feel free to suggest changes in the GitHub page, or any issues you spot/find.