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
|
/target
|
||||||
venv
|
venv
|
||||||
__pycache__
|
__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 import FastAPI, HTTPException
|
||||||
from fastapi.responses import RedirectResponse, FileResponse
|
from fastapi.responses import RedirectResponse, FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from dotenv import load_dotenv
|
||||||
import couchdb
|
import couchdb
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import validators
|
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 = FastAPI()
|
||||||
app.mount("/static", StaticFiles(directory="static", html=True), name="static")
|
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("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
@@ -16,8 +25,13 @@ async def root():
|
|||||||
|
|
||||||
@app.get("/{url_id}", status_code=301, response_class=RedirectResponse)
|
@app.get("/{url_id}", status_code=301, response_class=RedirectResponse)
|
||||||
async def redirect_urls(url_id):
|
async def redirect_urls(url_id):
|
||||||
target_url = couch["urls"][url_id].get('full_url')
|
db = couch["urls"]
|
||||||
|
if url_id in db:
|
||||||
|
target_url = db[url_id].get('full_url')
|
||||||
return RedirectResponse(target_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: Get JWT tokens working and use that to return only the user's urls
|
||||||
# TODO: Look into how FastAPI handles the Authentication header
|
# 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
|
# TODO: Calculate how many unique IDs we are actually generating
|
||||||
url_id = uuid.uuid4().hex[:6]
|
url_id = uuid.uuid4().hex[:6]
|
||||||
db[url_id] = { "full_url": body["url"], "user_id": body["username"] }
|
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:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="Url provided is invalid")
|
raise HTTPException(status_code=400, detail="Url provided is invalid")
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ GET :host/eecd98
|
|||||||
# Talk directly to CouchDB
|
# 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 value from key
|
||||||
GET http://admin:password@127.0.0.1:5984/urls/_all_docs?include_docs=true
|
GET http://admin:password@127.0.0.1:5984/urls/_all_docs?include_docs=true
|
||||||
:headers
|
: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
|
#+TODO: TODO INPROGRESS | DONE BACKLOG
|
||||||
#+STARTUP: overview
|
#+STARTUP: overview
|
||||||
|
|
||||||
* TODO Move everything into an app directory
|
* DONE Add button to copy shortened URL to clipboard
|
||||||
* TODO Knock out source code TODOs
|
* DONE Add a 404 page in case the URL provided is invalid
|
||||||
* TODO Add button to copy shortened URL to clipboard
|
* DONE RTFM on how to create docker compose files
|
||||||
* TODO Add a 404 page in case the URL provided is invalid
|
* DONE Containerize couchdb
|
||||||
* TODO Get the container running with the Dockerfile
|
* DONE Get the container running with the Dockerfile
|
||||||
* TODO Point zipmy.link to joe-vps
|
* DONE Point zipmy.link to joe-vps
|
||||||
- [ ] Add DNS records
|
- [X] Add DNS records
|
||||||
- [ ] Add to nginx.conf
|
- [X] Add to nginx.conf
|
||||||
- [ ] Generate certbot SSL cert
|
- [X] Generate certbot SSL cert
|
||||||
* TODO Create a README.org
|
* DONE Add MIT license
|
||||||
* TODO 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 RTFM on how to create a container network
|
||||||
* BACKLOG Gitea Actions
|
* BACKLOG Gitea Actions
|
||||||
* BACKLOG Unit tests
|
* BACKLOG Unit tests
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ uvicorn
|
|||||||
couchdb
|
couchdb
|
||||||
aiofiles
|
aiofiles
|
||||||
validators
|
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() {
|
async function shortenUrl() {
|
||||||
const url = document.getElementById("url-input").value;
|
const urlInput = document.getElementById("url-input");
|
||||||
const btn = document.getElementById("submit-btn").value;
|
const btn = document.getElementById("submit-btn");
|
||||||
// btn.enab
|
// btn.enab
|
||||||
const response = await fetch("/api/v1/urls", {
|
const response = await fetch("/api/v1/urls", {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { "Content-Type": "application/json" },
|
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 data = await response.json();
|
||||||
const resultElement = document.getElementById("result");
|
const resultElement = document.getElementById("result");
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
resultElement.textContent = `Shortened Url: ${data.shortenedUrl}`
|
resultElement.textContent = ''
|
||||||
resultElement.style.color = "black";
|
urlInput.value = data.shortenedUrl;
|
||||||
|
btn.innerText = "📋 Copy to Clipboard!";
|
||||||
|
btn.onclick = copyUrlToClipboard;
|
||||||
} else {
|
} else {
|
||||||
resultElement.textContent = `Error: ${data.detail}`
|
resultElement.textContent = `Error: ${data.detail}`
|
||||||
resultElement.style.color = "red";
|
resultElement.style.color = "red";
|
||||||
|
btn.value = "Shorten Url";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user