diff --git a/README.md b/README.md index d65723a..1a75f30 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,18 @@ Shortenit is a tool to shorten urls. **NOTE**: This is a very early draft project. Contributions are welcome. +## Install + +To install `shortenit` and all of its dependencies for development, run the following script. + +``` shell +$ scripts/install.sh +``` + ## Running -To run `shortenit`, edit the configuration file found in [config/config.yaml](config/config.yaml) then run the following commands. +To run `shortenit` for development, edit the configuration file found in [config/config.yaml](config/config.yaml) then run the following script. ```text -$ pip install -e . -$ shortenit +$ scripts/run.sh ``` diff --git a/config/config.yaml b/config/config.yaml index aaddc77..e868f41 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,10 +1,9 @@ Server: host: 127.0.0.1 port: 8000 - -Web: - host: 127.0.0.1 - port: 8000 + scheme: http + cors: False + static_folder: frontend/build Database: username: foo diff --git a/docs/conf.py b/docs/conf.py index 0a415e5..aa77d65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,29 +6,28 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'shortenit' -copyright = '2024, Elia el Lazkani' -author = 'Elia el Lazkani' -release = '0.0.0' +project = "shortenit" +copyright = "2024, Elia el Lazkani" +author = "Elia el Lazkani" +release = "0.0.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx_autodoc_typehints', + "sphinx.ext.autodoc", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx_autodoc_typehints", ] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' -html_static_path = ['_static'] +html_theme = "alabaster" +html_static_path = ["_static"] diff --git a/frontend/src/components/URLShortener/URLShortener.tsx b/frontend/src/components/URLShortener/URLShortener.tsx index 68cb781..7aa75e1 100644 --- a/frontend/src/components/URLShortener/URLShortener.tsx +++ b/frontend/src/components/URLShortener/URLShortener.tsx @@ -3,9 +3,9 @@ import "./URLShortener.css"; import axios from "axios"; import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -import QRCodeGenerator from "../qrCode/qrCodeGenerator"; +import Config from "../../config" -export default function () { +export default function URLShortener() { const [url, setUrl] = useState(""); const [shortenedUrl, setShortenedUrl] = useState(""); const [showInput, setShowInput] = useState(false); @@ -29,19 +29,20 @@ export default function () { } try { - const timestamp = Math.floor(Date.now() / 1000); + const config = Config() + const api_endpoint = "/api/v1/shorten"; + + const api_url = `${config.url}${api_endpoint}`; // Send the POST request to the backend await axios - .post("http://127.0.0.1:8000/shortenit", { - url: url, - timestamp: timestamp, + .post(api_url , { + url: url }) .then((response) => { if (response) { - const code: string = response.data.url; - const fullShortenedUrl: string = `http://127.0.0.1:8000/${code}`; - setShortenedUrl(fullShortenedUrl); + const shortUrl: string = response.data.url; + setShortenedUrl(shortUrl); setShowInput(true); } }); @@ -75,40 +76,39 @@ export default function () { } } return ( - <> -
-
-

Paste the URL

-
- setUrl(e.target.value)} - /> - -
- {shortenedUrl && ( -
- - -
- )} +
+
+

Paste the URL

+
+ setUrl(e.target.value)} + /> + +
+ + {shortenedUrl && showInput && ( +
+ + +
+ )} +
+
+
+

Shorten It

-
-
-

Shorten It

-
-
-

- Shortenit is a free and open-source URL shortening service - designed for simplicity and efficiency. -

-
+
+

+ Shortenit is a free and open-source URL shortening service designed + for simplicity and efficiency. +

- +
); } diff --git a/frontend/src/config.json b/frontend/src/config.json new file mode 100644 index 0000000..aba9fd4 --- /dev/null +++ b/frontend/src/config.json @@ -0,0 +1,12 @@ +{ + "frontend": { + "scheme": "http", + "host": "127.0.0.1", + "port": "8000" + }, + "api": { + "scheme": "http", + "host": "127.0.0.1", + "port": "8000" + } +} diff --git a/frontend/src/config.tsx b/frontend/src/config.tsx new file mode 100644 index 0000000..49984db --- /dev/null +++ b/frontend/src/config.tsx @@ -0,0 +1,24 @@ +import configuration from './config.json'; + +class APIConfig { + scheme: string; + host: string; + port: string; + url: string; + + constructor(scheme: string, host: string, port: string) { + this.scheme = scheme; + this.host = host; + this.port = port; + this.url = `${scheme}://${host}:${port}` + } +} + +export default function Config() { + const scheme = configuration.api.scheme; + const host = configuration.api.host; + const port = configuration.api.port; + + const apiConfig = new APIConfig(scheme, host, port); + return apiConfig +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..0ad942e --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cd frontend +npm install + +cd .. +poetry install diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..d9faa87 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cd frontend +npm run build +cd .. +poetry run shortenit "$@" diff --git a/shortenit/main.py b/shortenit/main.py index 6a9a7f6..7b8bc2b 100644 --- a/shortenit/main.py +++ b/shortenit/main.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 import argparse -import asyncio import logging import pathlib import sys -import time import typing from sqlalchemy import create_engine, exc @@ -53,9 +51,8 @@ def main() -> typing.NoReturn: sys.exit(0) -def shorten_it(config: dict, session: Session, data: str, ttl: int): - shortener_config = config.get("Shortener", None) - shortener = Shortener(session, shortener_config) +def shorten_it(config: dict, session: Session, data: str, ttl: int = 0): + shortener = Shortener(session, config) identifier = shortener.generate_uuid() if identifier: try: @@ -65,9 +62,9 @@ def shorten_it(config: dict, session: Session, data: str, ttl: int): _link = Link(data=data, pointers=[]) _pointer = Pointer(data=identifier, link_id=_link.id, link=_link, ttl=ttl) + session.add(_pointer) _link.pointers.append(_pointer) session.add(_link) - session.add(_pointer) session.commit() return _pointer.data return None diff --git a/shortenit/models/objects.py b/shortenit/models/objects.py index e7fc16c..716c107 100644 --- a/shortenit/models/objects.py +++ b/shortenit/models/objects.py @@ -21,7 +21,7 @@ class Pointer(base.Base): __tablename__ = "pointers" id: Mapped[int] = mapped_column(primary_key=True, index=True, init=False) data: Mapped[str] = mapped_column(index=True) - ttl: Mapped[str] + ttl: Mapped[int] link_id: Mapped[int] = mapped_column(ForeignKey("links.id")) link: Mapped["Link"] = relationship(back_populates="pointers") timestamp: Mapped[datetime] = mapped_column(default=func.now()) diff --git a/shortenit/templates/index.html b/shortenit/templates/index.html deleted file mode 100644 index 0ba5307..0000000 --- a/shortenit/templates/index.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - ShortenIt - - - - - - - - - - - - - - - - - - -
-
-
-

Shorten It!

-
-
- - -
-
-
-
- -
-
- -
-
-
-
- - - -
-
-
-
- -
-
-
- -
-
-
-
- - - - - diff --git a/shortenit/web.py b/shortenit/web.py index 17b11d2..f4920ab 100644 --- a/shortenit/web.py +++ b/shortenit/web.py @@ -1,10 +1,12 @@ import logging -import os from pathlib import Path -from flask_cors import CORS +from urllib.parse import urlunparse import trafaret -from flask import Flask, abort, redirect, render_template, request +from flask import Flask, abort, redirect, request, send_from_directory +from flask_cors import CORS + +from .common import check_file class Web: @@ -21,17 +23,29 @@ class Web: self.app.run(host=self.host, port=self.port, debug=self.debug) def init(self): + server_config = self.handler.configuration.get("Server", None) self.app = Flask(__name__) self.setup_routes() - CORS(self.app) + if server_config and server_config.get("cors", False): + self.logger.debug("Enabling CORS...") + CORS(self.app) def setup_routes(self): - self.app.add_url_rule("/", "/", self.handler.index, methods=["GET"]) self.app.add_url_rule( - "/shortenit", "/shortenit", self.handler.shortenit, methods=["POST"] + "/", "/", self.handler.index, methods=["GET"], defaults={"path": ""} + ) + self.app.add_url_rule("/", "/", self.handler.index, methods=["GET"]) + self.app.add_url_rule( + "/static/css/", "css", self.handler.css, methods=["GET"] ) self.app.add_url_rule( - "/", "/identifier", self.handler.short_redirect, methods=["GET"] + "/static/js/", "js", self.handler.js, methods=["GET"] + ) + self.app.add_url_rule( + "/api/v1/shorten", "shorten", self.handler.shortenit, methods=["POST"] + ) + self.app.add_url_rule( + "/r/", "redirect", self.handler.short_redirect, methods=["GET"] ) @@ -43,9 +57,22 @@ class SiteHandler: self.shorten_url = shorten_url self.lenghten_url = lenghten_url self.shortenit_load_format = trafaret.Dict( - {trafaret.Key("url"): trafaret.URL, trafaret.Key("timestamp"): trafaret.Int} + {trafaret.Key("url"): trafaret.URL} ) + def _get_server_config(self): + return self.configuration.get("Server", None) + + def _get_host(self): + host = self._get_server_config()["host"] + port = self._get_server_config()["port"] + scheme = self._get_server_config()["scheme"] + return scheme, host, port + + def _get_url(self, stub): + scheme, host, port = self._get_host() + return urlunparse((scheme, f"{host}:{port}", f"/r/{stub}", "", "", "")) + def shortenit(self): data = request.get_json() try: @@ -56,9 +83,12 @@ class SiteHandler: self.logger.error(e) abort(400) try: - short_url = self.shorten_url( - self.configuration, self.database, data["url"], data["timestamp"] + stub = self.shorten_url( + self.configuration.get("Shortener", None), + self.database, + data["url"], ) + short_url = self._get_url(stub) except KeyError as e: self.logger.error(e) abort(400) @@ -73,7 +103,29 @@ class SiteHandler: abort(404) return redirect(url) - def index(self): - return render_template("index.html") + def index(self, path): + if path != "": + return self._fetch_from_directory(path) + else: + return self._fetch_from_directory("index.html") + def css(self, path): + path = "static/css/" + path + return self._fetch_from_directory(path) + def js(self, path): + path = "static/js/" + path + return self._fetch_from_directory(path) + + def _fetch_from_directory(self, path): + try: + project_root = Path(__file__).parent.parent + static_folder = ( + f"{project_root}/" + self._get_server_config()["static_folder"] + ) + if check_file(static_folder + "/" + path): + return send_from_directory(static_folder, path) + else: + abort(404) + except: + abort(500)