Compare commits
10
Commits
9ed16ac6cd
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e05999df8 | ||
|
|
a9c88d68e3 | ||
|
|
ed5ca366fe | ||
|
|
505c201654 | ||
|
|
88582d9ff3 | ||
|
|
f3a858956e | ||
|
|
cd74846a7f | ||
|
|
52e38adcb7 | ||
|
|
1424fb2f43 | ||
|
|
de85790ffa |
No files matched your search
@@ -1,3 +1,5 @@
|
||||
/target
|
||||
venv
|
||||
__pycache__
|
||||
/.env
|
||||
/data/
|
||||
+13
-6
@@ -1,11 +1,18 @@
|
||||
FROM python:3.11
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3
|
||||
|
||||
WORKDIR /code
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
# Install any needed packages specified in requirements.txt
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
# Copy the current directory contents into the container at /app
|
||||
COPY . /app/
|
||||
|
||||
COPY ./app /code/app
|
||||
# Make port 8000 available to the world outside this container
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
# Run api.py when the container launches
|
||||
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "3003"]
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2023 Joseph Ferano - joseph@ferano.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#+TITLE: Zip My Link
|
||||
#+AUTHOR: Joseph Ferano
|
||||
#+OPTIONS: toc:nil
|
||||
|
||||
* About
|
||||
|
||||
This is a standard URL Shortening service built to practice web development.
|
||||
Currently it works at providing the most basic feature which is to give the user
|
||||
a shorter version of a URL they provide and then redirecting the user to the
|
||||
site when consuming the link.
|
||||
|
||||
** What I'm using
|
||||
- Python 3.11
|
||||
- FastAPI
|
||||
- CouchDB
|
||||
- ~podman~ and ~podman-compose~
|
||||
|
||||
* COMMENT Local variables
|
||||
;; Local Variables:
|
||||
;; eval: (olivetti-mode t)
|
||||
;; End:
|
||||
@@ -1,14 +1,23 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import RedirectResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from dotenv import load_dotenv
|
||||
import couchdb
|
||||
import os
|
||||
import uuid
|
||||
import validators
|
||||
|
||||
load_dotenv()
|
||||
|
||||
COUCHDB_USER = os.getenv("COUCHDB_USER")
|
||||
COUCHDB_PASSWORD = os.getenv("COUCHDB_PASSWORD")
|
||||
COUCHDB_HOST = os.getenv("COUCHDB_HOST")
|
||||
COUCHDB_PORT = os.getenv("COUCHDB_PORT")
|
||||
|
||||
app = FastAPI()
|
||||
app.mount("/static", StaticFiles(directory="static", html=True), name="static")
|
||||
|
||||
couch = couchdb.Server("http://admin:password@127.0.0.1:5984")
|
||||
couch = couchdb.Server(f"http://{COUCHDB_USER}:{COUCHDB_PASSWORD}@{COUCHDB_HOST}:{COUCHDB_PORT}")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
@@ -16,8 +25,13 @@ async def root():
|
||||
|
||||
@app.get("/{url_id}", status_code=301, response_class=RedirectResponse)
|
||||
async def redirect_urls(url_id):
|
||||
target_url = couch["urls"][url_id].get('full_url')
|
||||
return RedirectResponse(target_url)
|
||||
db = couch["urls"]
|
||||
if url_id in db:
|
||||
target_url = db[url_id].get('full_url')
|
||||
return RedirectResponse(target_url)
|
||||
else:
|
||||
return RedirectResponse('static/404.html')
|
||||
|
||||
|
||||
# TODO: Get JWT tokens working and use that to return only the user's urls
|
||||
# TODO: Look into how FastAPI handles the Authentication header
|
||||
@@ -43,7 +57,7 @@ async def create_url(body: dict):
|
||||
# TODO: Calculate how many unique IDs we are actually generating
|
||||
url_id = uuid.uuid4().hex[:6]
|
||||
db[url_id] = { "full_url": body["url"], "user_id": body["username"] }
|
||||
return { "shortenedUrl": "http://localhost:8000/" + url_id }
|
||||
return { "shortenedUrl": "https://zipmy.link/" + url_id }
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Url provided is invalid")
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ GET :host/eecd98
|
||||
# Talk directly to CouchDB
|
||||
###
|
||||
|
||||
# Get value from key
|
||||
GET http://admin:password@127.0.0.1:5984/_all_dbs
|
||||
:headers
|
||||
|
||||
# Get value from key
|
||||
GET http://admin:password@127.0.0.1:5984/urls/_all_docs?include_docs=true
|
||||
:headers
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
version: '3'
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./data:/opt/couchdb/data:Z
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
fastapi_app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- couchdb
|
||||
env_file:
|
||||
- .env
|
||||
+12
-13
@@ -2,20 +2,19 @@
|
||||
#+TODO: TODO INPROGRESS | DONE BACKLOG
|
||||
#+STARTUP: overview
|
||||
|
||||
* TODO Move everything into an app directory
|
||||
* TODO Knock out source code TODOs
|
||||
* TODO Add button to copy shortened URL to clipboard
|
||||
* TODO Add a 404 page in case the URL provided is invalid
|
||||
* TODO Get the container running with the Dockerfile
|
||||
* TODO Point zipmy.link to joe-vps
|
||||
- [ ] Add DNS records
|
||||
- [ ] Add to nginx.conf
|
||||
- [ ] Generate certbot SSL cert
|
||||
* TODO Create a README.org
|
||||
* TODO Add MIT license
|
||||
* DONE Add button to copy shortened URL to clipboard
|
||||
* DONE Add a 404 page in case the URL provided is invalid
|
||||
* DONE RTFM on how to create docker compose files
|
||||
* DONE Containerize couchdb
|
||||
* DONE Get the container running with the Dockerfile
|
||||
* DONE Point zipmy.link to joe-vps
|
||||
- [X] Add DNS records
|
||||
- [X] Add to nginx.conf
|
||||
- [X] Generate certbot SSL cert
|
||||
* DONE Add MIT license
|
||||
* INPROGRESS Create a README.org
|
||||
|
||||
|
||||
* BACKLOG Containerize couchdb
|
||||
* BACKLOG RTFM on how to create docker compose files
|
||||
* BACKLOG RTFM on how to create a container network
|
||||
* BACKLOG Gitea Actions
|
||||
* BACKLOG Unit tests
|
||||
|
||||
@@ -3,3 +3,4 @@ uvicorn
|
||||
couchdb
|
||||
aiofiles
|
||||
validators
|
||||
python-dotenv
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Zip My Link</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Zip My Link</h1>
|
||||
<h1>404</h1>
|
||||
<p>The shortened URL you provided was not found. Sorry about that!</p>
|
||||
<a href="index.html">zipmy.link</a>
|
||||
</body>
|
||||
</html>
|
||||
+18
-5
@@ -1,20 +1,33 @@
|
||||
function copyUrlToClipboard() {
|
||||
const urlInput = document.getElementById("url-input");
|
||||
urlInput.select();
|
||||
urlInput.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(urlInput.value);
|
||||
}
|
||||
|
||||
function resetLinkGeneration() {
|
||||
}
|
||||
|
||||
async function shortenUrl() {
|
||||
const url = document.getElementById("url-input").value;
|
||||
const btn = document.getElementById("submit-btn").value;
|
||||
const urlInput = document.getElementById("url-input");
|
||||
const btn = document.getElementById("submit-btn");
|
||||
// btn.enab
|
||||
const response = await fetch("/api/v1/urls", {
|
||||
method: 'PUT',
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: url, username: "anonymous" })
|
||||
body: JSON.stringify({ url: urlInput.value, username: "anonymous" })
|
||||
});
|
||||
const data = await response.json();
|
||||
const resultElement = document.getElementById("result");
|
||||
|
||||
if (response.ok) {
|
||||
resultElement.textContent = `Shortened Url: ${data.shortenedUrl}`
|
||||
resultElement.style.color = "black";
|
||||
resultElement.textContent = ''
|
||||
urlInput.value = data.shortenedUrl;
|
||||
btn.innerText = "📋 Copy to Clipboard!";
|
||||
btn.onclick = copyUrlToClipboard;
|
||||
} else {
|
||||
resultElement.textContent = `Error: ${data.detail}`
|
||||
resultElement.style.color = "red";
|
||||
btn.value = "Shorten Url";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user