Add in support for shortening links.

This commit is contained in:
Cameron Redmore 2025-02-15 13:10:26 +00:00
parent a2140625d4
commit d522eaadb2
6 changed files with 1062 additions and 25 deletions

View file

@ -1,3 +1,6 @@
node_modules/
.env .env
uploads uploads
node_modules
docker-compose.yml
Dockerfile
data

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
uploads uploads
node_modules node_modules
docker-compose.yml docker-compose.yml
data

View file

@ -13,4 +13,5 @@ services:
UPLOAD_DIR: /app/uploads # Container path for file uploads UPLOAD_DIR: /app/uploads # Container path for file uploads
volumes: volumes:
- ./uploads:/app/uploads # Binds the local "uploads" directory to UPLOAD_DIR - ./uploads:/app/uploads # Binds the local "uploads" directory to UPLOAD_DIR
- ./data:/app/data # Binds the local "data" directory to /app/data
restart: unless-stopped restart: unless-stopped

111
index.js
View file

@ -1,18 +1,22 @@
import Fastify from 'fastify' // Node.js built-in modules
import fastifyMultipart from '@fastify/multipart' import fs, { createWriteStream } from 'node:fs'
import dotenv from 'dotenv' import path from 'node:path'
import fs from 'node:fs'
import mime from 'mime-types'
import path, { join } from 'node:path'
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import sanitize from 'sanitize-filename'
import fastifyStatic from '@fastify/static'
import { exec } from 'child_process' import { exec } from 'child_process'
import { promisify } from 'node:util' import { promisify } from 'node:util'
import { pipeline } from 'node:stream/promises'
// Third-party modules
import dotenv from 'dotenv'
import Fastify from 'fastify'
import fastifyMultipart from '@fastify/multipart'
import fastifyStatic from '@fastify/static'
import mime from 'mime-types'
import sanitize from 'sanitize-filename'
import sharp from 'sharp' import sharp from 'sharp'
import sqlite3 from 'sqlite3'
const dbFile = path.join("data", 'database.db')
const db = new sqlite3.Database(dbFile)
const execAsync = promisify(exec) const execAsync = promisify(exec)
@ -46,12 +50,21 @@ fastify.get('/music.jpg', async (request, reply) => {
reply.header('Content-Type', 'image/jpeg').send(musicImage) reply.header('Content-Type', 'image/jpeg').send(musicImage)
}); });
const assetSizeCache = new Map() //Find the width and height of a file either an image or video, and cache the result in the db
//Find the width and height of a file either an image or video, and cache the result
async function getAssetSize(filePath) { async function getAssetSize(filePath) {
if (assetSizeCache.has(filePath)) { //Check if the asset size is already in the cache
return assetSizeCache.get(filePath) const cachedSize = await new Promise((resolve, reject) => {
db.get('SELECT * FROM asset_size_cache WHERE path = ?', [filePath], (err, row) => {
if (err) {
reject(err)
} else {
resolve(row)
}
})
})
if (cachedSize) {
return { width: cachedSize.width, height: cachedSize.height }
} }
const mimeType = mime.lookup(filePath) || 'application/octet-stream' const mimeType = mime.lookup(filePath) || 'application/octet-stream'
@ -77,7 +90,8 @@ async function getAssetSize(filePath) {
} }
} }
assetSizeCache.set(filePath, assetSize) //Cache the result
db.run('INSERT INTO asset_size_cache (path, width, height) VALUES (?, ?, ?)', [filePath, assetSize.width, assetSize.height])
return assetSize return assetSize
} }
@ -158,6 +172,27 @@ fastify.get('/s/:date/:filename', async (request, reply) => {
} }
}) })
//Route which will redirect to the original URL
fastify.get('/l/:shortId', async (request, reply) => {
const { shortId } = request.params
const url = await new Promise((resolve, reject) => {
db.get('SELECT url FROM short_urls WHERE short_id = ?', [shortId], (err, row) => {
if (err) {
reject(err)
} else {
resolve(row)
}
})
})
if (url) {
reply.redirect(url.url)
} else {
reply.code(404).send({ error: 'URL not found' })
}
});
const sizeLimit = process.env.FILE_SIZE_LIMIT || 16; //Size limit in GB const sizeLimit = process.env.FILE_SIZE_LIMIT || 16; //Size limit in GB
// Register multipart support // Register multipart support
@ -220,9 +255,51 @@ fastify.put('/upload', {
} }
}) })
//Route which will shorten a URL
fastify.put('/shorten', {
preHandler: async (request, reply) => {
const apiKey = request.headers.authorization
if (!apiKey || apiKey !== process.env.API_KEY) {
reply.code(401).send({ error: 'Unauthorized' })
}
}
}, async (request, reply) => {
const { url } = request.body
if (!url) {
reply.code(400).send({ error: 'No URL provided' })
return
}
//Short URL should be base62, 8 characters long
const shortId = Math.random().toString(36).substring(2, 10)
//Insert the short URL into the database
db.run('INSERT INTO short_urls (short_id, url) VALUES (?, ?)', [shortId, url])
const host = process.env.HOST || ''
reply.code(200).send({ url: host.replace(/\/$/, '') + '/l/' + shortId })
});
const initDb = async () => {
//Create asset_size_cache table
await db.run(`CREATE TABLE IF NOT EXISTS asset_size_cache (
path TEXT PRIMARY KEY,
width INTEGER,
height INTEGER
)`);
//Create short_urls table
await db.run(`CREATE TABLE IF NOT EXISTS short_urls (
short_id TEXT PRIMARY KEY,
url TEXT
)`);
}
// Start server // Start server
const start = async () => { const start = async () => {
try { try {
await initDb();
await fastify.listen({ host: process.env.LISTEN_HOST || '0.0.0.0', port: process.env.LISTEN_PORT || 3000 }) await fastify.listen({ host: process.env.LISTEN_HOST || '0.0.0.0', port: process.env.LISTEN_PORT || 3000 })
} catch (err) { } catch (err) {
fastify.log.error(err) fastify.log.error(err)

View file

@ -16,12 +16,14 @@
"fastify": "^5.2.1", "fastify": "^5.2.1",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sharp": "^0.33.5" "sharp": "^0.33.5",
"sqlite3": "^5.1.7"
}, },
"type": "module", "type": "module",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"sharp" "sharp",
"sqlite3"
] ]
} }
} }

953
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff