Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
|
bafb1c5e68 | ||
|
d9d763a1d1 | ||
|
e37aa4bb13 |
65 changed files with 478 additions and 20342 deletions
37
.gitignore
vendored
37
.gitignore
vendored
|
@ -1,40 +1,3 @@
|
|||
#Python
|
||||
.eggs/
|
||||
*.egg-info
|
||||
__pycache__/
|
||||
.mypy*/
|
||||
|
||||
# Sphinx
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.db
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
.pnp/
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage/
|
||||
|
||||
# production
|
||||
build/
|
||||
ui/
|
||||
dist/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
|
|
25
Dockerfile
25
Dockerfile
|
@ -1,25 +0,0 @@
|
|||
FROM node:23-alpine3.20 AS node
|
||||
|
||||
WORKDIR /shortenit
|
||||
COPY frontend /shortenit
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM python:3.13-alpine3.21 AS python
|
||||
|
||||
WORKDIR /shortenit
|
||||
COPY . /shortenit
|
||||
COPY --from=node /ui/ /shorthenit/ui/
|
||||
|
||||
RUN rm -rf dist/ && \
|
||||
pip install poetry && \
|
||||
poetry build
|
||||
|
||||
FROM python:3.13-alpine3.21
|
||||
|
||||
COPY --from=python /shortenit/dist/*.tar.gz /shortenit/
|
||||
|
||||
RUN pip install /shortenit/*.tar.gz && \
|
||||
rm -rf /shortenit
|
||||
|
||||
ENTRYPOINT ["shortenit"]
|
25
LICENSE
25
LICENSE
|
@ -1,25 +0,0 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2019, Elia El Lazkani
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
21
README.md
21
README.md
|
@ -1,21 +0,0 @@
|
|||
# Shortenit
|
||||
|
||||
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` for development, edit the configuration file found in [config/config.yaml](config/config.yaml) then run the following script.
|
||||
|
||||
```text
|
||||
$ scripts/run.sh
|
||||
```
|
6
README.rst
Normal file
6
README.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
shortenit
|
||||
=========
|
||||
|
||||
shortenit is a tool to shorten urls.
|
||||
|
||||
NOTE: This is a very early draft project. Contributions are welcome.
|
|
@ -1,22 +1,7 @@
|
|||
Server:
|
||||
hostname: localhost
|
||||
bind_ip: 0.0.0.0
|
||||
host: 127.0.0.1
|
||||
port: 8000
|
||||
scheme: http
|
||||
cors: False
|
||||
enable_ui: True
|
||||
static_folder: ui/
|
||||
|
||||
Database:
|
||||
username: foo
|
||||
password: bar
|
||||
#url: "sqlite+pysqlite:///:memory:"
|
||||
url: "sqlite+pysqlite:///shortenit.db"
|
||||
|
||||
Shortener:
|
||||
# *CAUTION*: Enabling this check if the ID already exists before returning it.
|
||||
# Even though this guarantees that the ID doesn't exist, this might inflict
|
||||
# some performance hit.
|
||||
id_length: 32
|
||||
check_duplicate_id: True
|
||||
id_upper_case: False
|
||||
CouchDB:
|
||||
username: root
|
||||
password: root
|
||||
url: http://localhost:5984
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR =
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -1,5 +0,0 @@
|
|||
Common
|
||||
======
|
||||
|
||||
.. automodule:: shortenit.common
|
||||
:members:
|
33
docs/conf.py
33
docs/conf.py
|
@ -1,33 +0,0 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "shortenit"
|
||||
copyright = "2025, 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",
|
||||
]
|
||||
|
||||
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"]
|
|
@ -1,5 +0,0 @@
|
|||
Config
|
||||
======
|
||||
|
||||
.. automodule:: shortenit.config
|
||||
:members:
|
|
@ -1,31 +0,0 @@
|
|||
.. shortenit documentation master file, created by
|
||||
sphinx-quickstart on Sat Nov 30 11:26:49 2024.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Shortenit Documentation
|
||||
=======================
|
||||
|
||||
``Shortenit`` is a tool to shorten urls.
|
||||
|
||||
**NOTE**: This is a very early draft project. Contributions are welcome.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
common
|
||||
config
|
||||
logger
|
||||
main
|
||||
pointer
|
||||
shortener
|
||||
web
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
|
@ -1,5 +0,0 @@
|
|||
Logger
|
||||
======
|
||||
|
||||
.. automodule:: shortenit.logger
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
Main
|
||||
====
|
||||
|
||||
.. automodule:: shortenit.main
|
||||
:members:
|
|
@ -1,35 +0,0 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=docs
|
||||
set BUILDDIR=build
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
|
@ -1,5 +0,0 @@
|
|||
Pointer
|
||||
=======
|
||||
|
||||
.. automodule:: shortenit.pointer
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
Shortener
|
||||
=========
|
||||
|
||||
.. automodule:: shortenit.models.shortener
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
Web
|
||||
===
|
||||
|
||||
.. automodule:: shortenit.web
|
||||
:members:
|
|
@ -1,46 +0,0 @@
|
|||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
17501
frontend/package-lock.json
generated
17501
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,53 +0,0 @@
|
|||
{
|
||||
"name": "shortenit",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.121",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"axios": "^1.7.9",
|
||||
"framer-motion": "^11.15.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-toastify": "^11.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "BUILD_PATH='../ui/' react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.15"
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Shorten It</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&family=Outfit:wght@100..900&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&family=Outfit:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap");
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: "Outfit", sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-primary: #f5f5f5;
|
||||
--color-secondary: #19253a;
|
||||
--color-tertiary: #1d4ed8;
|
||||
--text-color: #333;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import React from "react";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import "./App.css";
|
||||
import AppContent from "./AppContent";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<Router>
|
||||
<AppContent />
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,26 +0,0 @@
|
|||
import { Routes, Route, useLocation } from "react-router-dom";
|
||||
import "./App.css";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
|
||||
// Pages & Components
|
||||
import Navbar from "./components/navbar/Navbar";
|
||||
import Footer from "./components/footer/Footer";
|
||||
import Home from "./pages/home/Home";
|
||||
import Features from "./pages/features/Features";
|
||||
import Contact from "./pages/contact/Contact";
|
||||
|
||||
export default function AppContent() {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<AnimatePresence mode="wait" />
|
||||
<Routes location={location} key={location.pathname}>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/features" element={<Features />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
.URLShortener-component {
|
||||
height: 300px;
|
||||
width: 800px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.URLShortener-component:hover {
|
||||
box-shadow: 0px 20px 25px rgba(0, 0, 0, 0.3), 0px 8px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.left-side {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
padding-bottom: 20%;
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
.left-side h1 {
|
||||
color: var(--color-tertiary);
|
||||
border-bottom: 1px solid var(--color-tertiary);
|
||||
}
|
||||
|
||||
.url-input {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.url-input input {
|
||||
width: 250px;
|
||||
height: 40px;
|
||||
outline: none;
|
||||
border-radius: 5px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.4);
|
||||
padding-left: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.url-input button {
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background-color: var(--color-tertiary);
|
||||
border: none;
|
||||
min-width: 80px;
|
||||
color: #fff;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.url-input button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border-left: 2px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 0 5%;
|
||||
}
|
||||
|
||||
.right-side-title h1 {
|
||||
color: var(--color-tertiary);
|
||||
}
|
||||
|
||||
.right-side-content p {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 14px;
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
@media (max-width: 730px) {
|
||||
.URLShortener-component {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.left-side {
|
||||
padding-bottom: 5%;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
border-left: none;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 475px) {
|
||||
.url-input input {
|
||||
height: 25px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.left-side h1 {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.right-side-title h1 {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.right-side-content p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.url-input button {
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 475px) {
|
||||
.url-input {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.url-input button {
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.url-input input {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-toast {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.url-input.shortened {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
animation: fadeIn 0.3s forwards;
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import "./URLShortener.css";
|
||||
import axios from "axios";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import Config from "../../config"
|
||||
|
||||
export default function URLShortener() {
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [shortenedUrl, setShortenedUrl] = useState<string>("");
|
||||
const [showInput, setShowInput] = useState<boolean>(false);
|
||||
|
||||
async function ShortenIt(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!url) {
|
||||
toast.error("Please provide a URL", {
|
||||
position: "top-right",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "dark",
|
||||
className: "custom-toast",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = Config()
|
||||
const api_endpoint = "/api/v1/shorten";
|
||||
|
||||
const api_url = `${config.url}${api_endpoint}`;
|
||||
|
||||
// Send the POST request to the backend
|
||||
const response = await axios.post(api_url , {url: url})
|
||||
if (response.data.url) {
|
||||
const shortUrl: string = response.data.url;
|
||||
setShortenedUrl(shortUrl);
|
||||
setShowInput(true);
|
||||
toast.success("URL Shortened Successfully", {
|
||||
position: "top-right",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "dark",
|
||||
className: "custom-toast",
|
||||
});
|
||||
} else {
|
||||
toast.error("Something went wrong while shortening the URL", {
|
||||
position: "top-right",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "dark",
|
||||
className: "custom-toast",
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setShowInput(false);
|
||||
}, 900000);
|
||||
} catch (error : any) {
|
||||
if (error.response) {
|
||||
const errorMessage : string = error.response.data.msg || "An error occurred. Please try again.";
|
||||
toast.error(errorMessage, {
|
||||
position: "top-right",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "dark",
|
||||
className: "custom-toast",
|
||||
});
|
||||
} else {
|
||||
toast.error("Network error. Please check your connection and try again.", {
|
||||
position: "top-right",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "dark",
|
||||
className: "custom-toast",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyURL() {
|
||||
try {
|
||||
if (shortenedUrl) {
|
||||
await navigator.clipboard.writeText(shortenedUrl);
|
||||
toast.success("Copied to Clipboard", {
|
||||
position: "top-right",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "dark",
|
||||
className: "custom-toast",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="URLShortener-component">
|
||||
<div className="left-side">
|
||||
<h1>Paste the URL</h1>
|
||||
<form className="url-input" onSubmit={ShortenIt}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://www.example.com"
|
||||
aria-label="URL input field"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<button type="submit">Shorten It</button>
|
||||
</form>
|
||||
|
||||
{shortenedUrl && showInput && (
|
||||
<div className="url-input shortened">
|
||||
<input type="text" readOnly value={shortenedUrl} />
|
||||
<button onClick={copyURL}>Copy</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="right-side">
|
||||
<div className="right-side-title">
|
||||
<h1>Shorten It</h1>
|
||||
</div>
|
||||
<div className="right-side-content">
|
||||
<p>
|
||||
Shortenit is a free and open-source URL shortening service designed
|
||||
for simplicity and efficiency.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
export interface CardProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
link : string;
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
.Card {
|
||||
position: relative;
|
||||
height: 270px;
|
||||
width: 250px;
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Roboto", sans-serif;
|
||||
padding: 15px;
|
||||
transition: .3s, transform .6s ease;
|
||||
}
|
||||
|
||||
.Card:hover {
|
||||
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.4), 0px 4px 6px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-25px);
|
||||
}
|
||||
|
||||
|
||||
.top-part {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 30px;
|
||||
color: var(--color-primary);
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-tertiary);
|
||||
}
|
||||
|
||||
.bottom-part {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.feature-button button {
|
||||
padding: 8px 30px;
|
||||
border-radius: 5px;
|
||||
background-color: #1d4ed8;
|
||||
border: 1px solid #f5f5f5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.feature-button button a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.feature-button button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import React from "react";
|
||||
import "./FeaturesCard.css";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { CardProps } from "./CardProps";
|
||||
|
||||
const FeaturesCard: React.FC<CardProps> = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="Card" onClick={() => navigate(`${link}`)}>
|
||||
<div className="top-part">
|
||||
<div className="feature-icon">{icon}</div>
|
||||
</div>
|
||||
|
||||
<div className="bottom-part">
|
||||
<div className="feature-title">
|
||||
<h3>{title}</h3>
|
||||
</div>
|
||||
|
||||
<div className="feature-description">
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="feature-button">
|
||||
<button>
|
||||
<Link to={link}>Read More</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesCard;
|
|
@ -1,8 +0,0 @@
|
|||
.footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: var(--color-secondary);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import React from "react";
|
||||
import "./Footer.css";
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="footer"></footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
|
@ -1,91 +0,0 @@
|
|||
.navbar {
|
||||
background-color: #19253a;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar-container ul {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.navbar-container ul li {
|
||||
padding: 8px 15px;
|
||||
border-radius: 5px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.navbar-container ul li a {
|
||||
color: #fff;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar-container ul li:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
color: #fff;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.navbar-btn {
|
||||
padding: 8px 30px;
|
||||
border-radius: 5px;
|
||||
background-color: #1d4ed8;
|
||||
border: 1px solid #f5f5f5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hamburger {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-container ul {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navbar-container ul.open {
|
||||
display: flex;
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import "./Navbar.css";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FaBars } from "react-icons/fa6";
|
||||
|
||||
function Navbar() {
|
||||
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||
|
||||
const toggleMenu = (): void => {
|
||||
setMenuOpen((prevState) => !prevState);
|
||||
};
|
||||
return (
|
||||
<header className="navbar">
|
||||
<div className="logo">
|
||||
<Link to="/">Shorten It</Link>
|
||||
</div>
|
||||
|
||||
<FaBars className="hamburger" onClick={toggleMenu} />
|
||||
|
||||
<nav className="navbar-container">
|
||||
<ul className={menuOpen ? "open" : ""}>
|
||||
<li>
|
||||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/features">Features</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/contact">Contact</Link>
|
||||
</li>
|
||||
|
||||
<button className="navbar-btn">Login</button>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"frontend": {
|
||||
"scheme": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": "8000"
|
||||
},
|
||||
"api": {
|
||||
"scheme": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": "8000"
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
@ -1,6 +0,0 @@
|
|||
import React from "react";
|
||||
import "./Contact.css";
|
||||
|
||||
export default function Contact() {
|
||||
return <h1>CONTACT</h1>;
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
.features-header {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: "Roboto", sans-serif;
|
||||
background-color: var(--color-primary);
|
||||
margin-bottom: 75px;
|
||||
}
|
||||
|
||||
.features-header h2 {
|
||||
font-size: 25px;
|
||||
color: var(--color-tertiary);
|
||||
}
|
||||
|
||||
.features-header h1 {
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
.features-body {
|
||||
min-height: 100vh;
|
||||
margin: 0 10%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-row-gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1466px) {
|
||||
.features-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1076px) {
|
||||
.features-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 708px) {
|
||||
.features-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-row-gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 390px) {
|
||||
.features-body {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.features-header {
|
||||
margin-bottom: -50%;
|
||||
}
|
||||
|
||||
.Feature-Card {
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 3/2;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.features-header h1 {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import React from "react";
|
||||
import "./Features.css";
|
||||
import FeaturesCard from "../../components/featuresCard/FeaturesCard";
|
||||
import { FaLink } from "react-icons/fa";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
function Features() {
|
||||
return (
|
||||
<>
|
||||
<motion.section
|
||||
className="features-header"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.7, ease: "easeInOut" }}
|
||||
>
|
||||
<h2>Features</h2>
|
||||
<h1>Our Services & Features</h1>
|
||||
</motion.section>
|
||||
|
||||
<motion.section
|
||||
className="features-body"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1.5, ease: "easeInOut" }}
|
||||
>
|
||||
<div className="Feature-Card">
|
||||
<FeaturesCard
|
||||
icon={<FaLink />}
|
||||
title="URL Shortener"
|
||||
description="Transform long URLs into short, shareable links in a blink of an eye."
|
||||
link="/"
|
||||
/>
|
||||
</div>
|
||||
</motion.section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Features;
|
|
@ -1,8 +0,0 @@
|
|||
.home-page {
|
||||
height: 80vh;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import React from "react";
|
||||
import "./Home.css";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import URLShortener from "../../components/URLShortener/URLShortener";
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<motion.div
|
||||
className="home-page"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1.5, ease: "easeInOut" }}
|
||||
>
|
||||
<URLShortener />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
1139
poetry.lock
generated
1139
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,64 +0,0 @@
|
|||
[tool.poetry]
|
||||
name = "shortenit"
|
||||
version = "0.0.0"
|
||||
description = "Shortenit is a tool to shorten urls"
|
||||
authors = ["Elia el Lazkani <git@lazkani.io>"]
|
||||
maintainers = ["Elia el Lazkani <git@lazkani.io>", "Anthony Al Lazkani <anthonylazkani.22@gmail.com>"]
|
||||
repository = "https://scm.project42.io/elia/shortenit"
|
||||
license = "BSD-2-Clause"
|
||||
readme = "README.md"
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Information Technology",
|
||||
"Intended Audience :: System Administrators"
|
||||
]
|
||||
packages = [
|
||||
{ include = "shortenit" },
|
||||
]
|
||||
include = [
|
||||
{ path = "ui/", format = ["sdist", "wheel"] },
|
||||
{ path = "config/", format = ["sdist", "wheel"] },
|
||||
]
|
||||
exclude = [
|
||||
".gitignore",
|
||||
"pyproject.toml",
|
||||
"poetry.lock",
|
||||
"docs/",
|
||||
"scripts/",
|
||||
"frontend/",
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
shortenit = "shortenit.main:main"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
PyYAML = "^6.0.2"
|
||||
sqlalchemy = "^2.0.36"
|
||||
pydantic = "^2.10.2"
|
||||
Flask = "^3.1.0"
|
||||
flask-cors = "^5.0.0"
|
||||
trafaret = "^2.1.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.10.0"
|
||||
isort = "^5.13.2"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
sphinx = "^8.1.3"
|
||||
sphinx-autodoc-typehints = "^2.5.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
2
requirements/development.txt
Normal file
2
requirements/development.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
-r requirements.txt
|
||||
setuptools-git
|
4
requirements/requirements.txt
Normal file
4
requirements/requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
pyyaml
|
||||
cloudant
|
||||
flask
|
||||
trafaret
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd frontend
|
||||
npm install
|
||||
|
||||
cd ..
|
||||
poetry install
|
|
@ -1,6 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd frontend
|
||||
npm run build
|
||||
cd ..
|
||||
poetry run shortenit "$@"
|
48
setup.py
Normal file
48
setup.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from pathlib import Path
|
||||
from setuptools import setup
|
||||
|
||||
#<link rel=stylesheet type=text/css href="{{ url('static', filename='css/custom.css') }}">
|
||||
here = Path(__file__).absolute().parent
|
||||
|
||||
with open(str(here.joinpath('README.rst')), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
with open(str(here.joinpath('requirements/requirements.txt')),
|
||||
encoding='utf-8') as f:
|
||||
install_requires = f.read()
|
||||
|
||||
description = 'Shortenit will shorten that url for you'
|
||||
|
||||
setup(
|
||||
name='shortenit',
|
||||
exclude_package_data={'': ['.gitignore', 'requirements/', 'setup.py']},
|
||||
version_format='{tag}_{gitsha}',
|
||||
setup_requires=['setuptools-git', 'setuptools-git-version'],
|
||||
license='BSD',
|
||||
author='Elia El Lazkani',
|
||||
author_email='eliaellazkani@gmail.com',
|
||||
url='https://gitlab.com/elazkani/shortenit',
|
||||
python_requires='>=python3.6',
|
||||
description=description,
|
||||
long_description=long_description,
|
||||
install_requires=install_requires,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'shortenit = shortenit.main:main'
|
||||
]
|
||||
},
|
||||
packages=['shortenit'],
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Information Technology',
|
||||
'Intended Audience :: System Administrators'
|
||||
],
|
||||
zip_safe=False
|
||||
)
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
# Setup logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
@ -2,45 +2,21 @@ import yaml
|
|||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
Configuration importer.
|
||||
"""
|
||||
|
||||
def __init__(self, config_path: str):
|
||||
"""
|
||||
Initialize the configuration importer.
|
||||
|
||||
:param config_path: The path of the configuration file.
|
||||
"""
|
||||
self.config_path = config_path
|
||||
self.config = None
|
||||
|
||||
def get_config(self) -> str:
|
||||
"""
|
||||
Get the configuration saved in the configuration file.
|
||||
|
||||
:returns: The configuration saved in the configuration file.
|
||||
"""
|
||||
def get_config(self):
|
||||
if not self.config:
|
||||
_config = self.load_config()
|
||||
self.config = _config
|
||||
return self.config
|
||||
|
||||
def load_config(self) -> str:
|
||||
"""
|
||||
Read the configuration saved in the configuration file.
|
||||
|
||||
:returns: The configuration saved in the configruation file.
|
||||
"""
|
||||
with open(self.config_path, "rt") as f:
|
||||
config = yaml.safe_load(f)
|
||||
def load_config(self):
|
||||
with open(self.config_path, 'rt') as f:
|
||||
config = yaml.load(f)
|
||||
if self.validate_config(config):
|
||||
return config
|
||||
|
||||
def validate_config(self, config: str) -> bool:
|
||||
"""
|
||||
Validate the configuration.
|
||||
|
||||
:returns: Configuration validation status.
|
||||
"""
|
||||
def validate_config(self, config):
|
||||
return True
|
||||
|
|
29
shortenit/counter.py
Normal file
29
shortenit/counter.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import logging
|
||||
|
||||
from cloudant.document import Document
|
||||
|
||||
|
||||
class Counter:
|
||||
def __init__(self, counter_db):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.counter_db = counter_db
|
||||
self.counter = None
|
||||
|
||||
def get_counter(self) -> int:
|
||||
with Document(self.counter_db, 'counter') as counter:
|
||||
self.logger.debug("Counter: %s", counter)
|
||||
try:
|
||||
self.counter = counter['value']
|
||||
except KeyError:
|
||||
self.logger.warn(
|
||||
"Counter was not initialized, initializing...")
|
||||
counter['value'] = 0
|
||||
try:
|
||||
counter['value'] += 1
|
||||
except Exception as e:
|
||||
self.logger.err(e)
|
||||
# Need to check if the value exists or not as to not jump values
|
||||
# which it currently does but it's not a big issue for right now
|
||||
self.counter = counter['value']
|
||||
|
||||
return self.counter
|
57
shortenit/data.py
Normal file
57
shortenit/data.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
import time
|
||||
import logging
|
||||
|
||||
from hashlib import sha256
|
||||
from cloudant.document import Document
|
||||
|
||||
|
||||
class Data:
|
||||
def __init__(self, data_db: object,
|
||||
identifier: str = None,
|
||||
data: str = None):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.data_db = data_db
|
||||
self.identifier = identifier
|
||||
self.data = data
|
||||
self.timestamp = time.time()
|
||||
self.pointers = []
|
||||
self.data_found = None
|
||||
self.populate()
|
||||
|
||||
def generate_identifier(self):
|
||||
hash_object = sha256(self.data.encode('utf-8'))
|
||||
self.identifier = hash_object.hexdigest()
|
||||
|
||||
def populate(self, pointer: str = None):
|
||||
if self.identifier:
|
||||
self.logger.debug("The identifier is set, retrieving data...")
|
||||
self.get_data()
|
||||
elif self.data:
|
||||
self.logger.debug("The data is set, generating an identifier...")
|
||||
self.generate_identifier()
|
||||
self.logger.debug("Attempting to get the data with "
|
||||
"the identifier generated...")
|
||||
self.get_data()
|
||||
if not self.data_found:
|
||||
self.logger.debug("The data generated is not found, "
|
||||
"creating...")
|
||||
self.set_data(pointer)
|
||||
|
||||
def get_data(self):
|
||||
with Document(self.data_db, self.identifier) as data:
|
||||
try:
|
||||
self.data = data['value']
|
||||
self.timestamp = data['timestamp']
|
||||
self.pointers = data['pointers']
|
||||
self.data_found = True
|
||||
except KeyError:
|
||||
self.data_found = False
|
||||
|
||||
def set_data(self, pointer):
|
||||
with Document(self.data_db, self.identifier) as data:
|
||||
data['value'] = self.data
|
||||
data['timestamp'] = self.timestamp
|
||||
try:
|
||||
data['pointers'].append(pointer)
|
||||
except KeyError:
|
||||
data['pointers'] = [pointer]
|
57
shortenit/db.py
Normal file
57
shortenit/db.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
import logging
|
||||
|
||||
from cloudant.client import CouchDB
|
||||
|
||||
|
||||
class DB:
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.username = config['username']
|
||||
self.password = config['password']
|
||||
self.url = config['url']
|
||||
self.client = None
|
||||
self.session = None
|
||||
|
||||
def initialize_shortenit(self):
|
||||
try:
|
||||
self.counter_db = self.client['counter']
|
||||
except KeyError:
|
||||
self.logger.warn(
|
||||
"The 'counter' database was not found, creating...")
|
||||
self.counter_db = self.client.create_database('counter')
|
||||
if self.counter_db.exists():
|
||||
self.logger.info(
|
||||
"The 'counter' database was successfully created.")
|
||||
|
||||
try:
|
||||
self.data_db = self.client['data']
|
||||
except KeyError:
|
||||
self.logger.warn(
|
||||
"The 'data' database was not found, creating...")
|
||||
self.data_db = self.client.create_database('data')
|
||||
if self.data_db.exists():
|
||||
self.logger.info(
|
||||
"The 'data' database was successfully created.")
|
||||
|
||||
try:
|
||||
self.pointers_db = self.client['pointers']
|
||||
except KeyError:
|
||||
self.logger.warn(
|
||||
"The 'pointers' database was not found, creating...")
|
||||
self.pointers_db = self.client.create_database('pointers')
|
||||
if self.pointers_db.exists():
|
||||
self.logger.info(
|
||||
"The 'pointers' database was successfully created.")
|
||||
|
||||
def __enter__(self) -> CouchDB:
|
||||
"""
|
||||
"""
|
||||
self.client = CouchDB(self.username, self.password,
|
||||
url=self.url, connect=True)
|
||||
self.session = self.client.session()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
"""
|
||||
"""
|
||||
self.client.disconnect()
|
|
@ -1,17 +1,12 @@
|
|||
import logging.config
|
||||
import os
|
||||
import typing
|
||||
|
||||
import yaml
|
||||
|
||||
import logging.config
|
||||
from .common import check_file
|
||||
|
||||
|
||||
def setup_logging(
|
||||
default_path: str = None,
|
||||
def setup_logging(default_path: str = None,
|
||||
default_level: int = logging.ERROR,
|
||||
env_key: str = "LOG_CFG",
|
||||
) -> typing.NoReturn:
|
||||
env_key: str = 'LOG_CFG') -> None:
|
||||
"""
|
||||
Method that sets the logging system up.
|
||||
|
||||
|
@ -34,12 +29,10 @@ def setup_logging(
|
|||
path = None
|
||||
|
||||
if path:
|
||||
with open(path, mode="r") as f:
|
||||
with open(path, mode='r') as f:
|
||||
config = yaml.safe_load(f.read())
|
||||
logging.config.dictConfig(config)
|
||||
else:
|
||||
_format = (
|
||||
"%(asctime)s - %(levelname)s - %(filename)s:"
|
||||
"%(name)s.%(funcName)s:%(lineno)d - %(message)s"
|
||||
)
|
||||
_format = '%(asctime)s - %(levelname)s - %(filename)s:' \
|
||||
'%(name)s.%(funcName)s:%(lineno)d - %(message)s'
|
||||
logging.basicConfig(level=default_level, format=_format)
|
||||
|
|
|
@ -2,27 +2,26 @@
|
|||
import argparse
|
||||
import logging
|
||||
import pathlib
|
||||
import sys
|
||||
import typing
|
||||
import asyncio
|
||||
|
||||
from sqlalchemy import create_engine, exc
|
||||
from sqlalchemy.orm import Session
|
||||
import time
|
||||
|
||||
from shortenit.config import Config
|
||||
from shortenit.logger import setup_logging
|
||||
from shortenit.models.base import Base
|
||||
from shortenit.models.objects import Link, Pointer
|
||||
from shortenit.models.shortener import Shortener
|
||||
from shortenit.web import SiteHandler, Web
|
||||
from .data import Data
|
||||
from .pointer import Pointer
|
||||
from .config import Config
|
||||
from .counter import Counter
|
||||
from .db import DB
|
||||
from .logger import setup_logging
|
||||
from .web import Web, SiteHandler
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).parent.parent
|
||||
CONFIGURATION = f"{PROJECT_ROOT}/config/config.yaml"
|
||||
CONFIGURATION = f'{PROJECT_ROOT}/config/config.yaml'
|
||||
|
||||
# Setup logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main() -> typing.NoReturn:
|
||||
def main() -> None:
|
||||
"""
|
||||
Main method
|
||||
"""
|
||||
|
@ -31,56 +30,40 @@ def main() -> typing.NoReturn:
|
|||
debug = True if args.verbose > 0 else False
|
||||
verbosity_level = verbosity(args.verbose)
|
||||
setup_logging(args.logger, verbosity_level)
|
||||
if args.config:
|
||||
config = Config(args.config).get_config()
|
||||
else:
|
||||
config = Config(CONFIGURATION).get_config()
|
||||
db_config = config.get("Database", None)
|
||||
server_config = config.get("Server", None)
|
||||
db_config = config.get('CouchDB', None)
|
||||
server_config = config.get('Server', None)
|
||||
if db_config:
|
||||
try:
|
||||
engine = create_engine(db_config["url"], echo=False, future=True)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
with DB(db_config) as db:
|
||||
db.initialize_shortenit()
|
||||
|
||||
with Session(bind=engine, autoflush=True, future=True) as session:
|
||||
handler = SiteHandler(config, session, shorten_it, lengthen_it)
|
||||
handler = SiteHandler(db, shorten_url, lenghten_url)
|
||||
web = Web(handler, debug=debug)
|
||||
web.host = server_config.get("bind_ip", None)
|
||||
web.port = server_config.get("port", None)
|
||||
web.host = server_config.get('host', None)
|
||||
web.port = server_config.get('port', None)
|
||||
web.start_up()
|
||||
|
||||
except:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
def shorten_url(database: DB, data: str, ttl: time.time):
|
||||
counter = Counter(database.counter_db)
|
||||
data = Data(database.data_db,
|
||||
data=data)
|
||||
data.populate()
|
||||
pointer = Pointer(database.pointers_db, counter)
|
||||
pointer.generate_pointer(
|
||||
data.identifier,
|
||||
ttl
|
||||
)
|
||||
data.set_data(pointer.identifier)
|
||||
return pointer.identifier
|
||||
|
||||
|
||||
def shorten_it(config: dict, session: Session, data: str, ttl: int = 0):
|
||||
shortener = Shortener(session, config)
|
||||
identifier = shortener.generate_uuid()
|
||||
if identifier:
|
||||
try:
|
||||
_link = session.query(Link).filter_by(data=data).one()
|
||||
except exc.NoResultFound:
|
||||
logger.debug("Link '%s' was not found in the database.", data)
|
||||
_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.commit()
|
||||
return _pointer.data
|
||||
return None
|
||||
|
||||
|
||||
def lengthen_it(session: Session, identifier: str):
|
||||
try:
|
||||
_pointer = session.query(Pointer).filter_by(data=identifier).one()
|
||||
except exc.NoResultFound:
|
||||
logger.debug("Pointer '%s' was not found in the database.", identifier)
|
||||
return None
|
||||
|
||||
return _pointer.link.data
|
||||
def lenghten_url(database: DB, identifier: str):
|
||||
pointer = Pointer(database.pointers_db)
|
||||
pointer.get_pointer(identifier)
|
||||
data = Data(database.data_db, identifier=pointer.data_hash)
|
||||
data.populate()
|
||||
return data.data
|
||||
|
||||
|
||||
def argument_parse() -> argparse.ArgumentParser:
|
||||
|
@ -90,18 +73,15 @@ def argument_parse() -> argparse.ArgumentParser:
|
|||
:returns: The argument parser.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generates rundeck resources " "file from different API sources."
|
||||
)
|
||||
description="Generates rundeck resources "
|
||||
"file from different API sources.")
|
||||
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="Verbosity level to use."
|
||||
)
|
||||
'-v', '--verbose', action='count', default=0,
|
||||
help='Verbosity level to use.')
|
||||
parser.add_argument(
|
||||
"-l", "--logger", type=str, help="The logger YAML configuration file."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--config", type=str, help="The shortenit configuration file."
|
||||
)
|
||||
'-l', '--logger', type=str,
|
||||
help='The logger YAML configuration file.')
|
||||
return parser
|
||||
|
||||
|
||||
|
@ -120,7 +100,3 @@ def verbosity(verbose: int):
|
|||
return logging.INFO
|
||||
elif verbose > 2:
|
||||
return logging.DEBUG
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
|
||||
|
||||
|
||||
class Base(MappedAsDataclass, DeclarativeBase):
|
||||
pass
|
|
@ -1,27 +0,0 @@
|
|||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from pydantic import AnyHttpUrl
|
||||
from sqlalchemy import DateTime, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
import shortenit.models.base as base
|
||||
|
||||
|
||||
class Link(base.Base):
|
||||
__tablename__ = "links"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True, init=False)
|
||||
data: Mapped[AnyHttpUrl] = mapped_column(String, index=True)
|
||||
pointers: Mapped[List["Pointer"]] = relationship(back_populates="link")
|
||||
timestamp: Mapped[datetime] = mapped_column(default=func.now())
|
||||
|
||||
|
||||
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[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())
|
|
@ -1,92 +0,0 @@
|
|||
import logging
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
import shortenit.models.objects as objects
|
||||
|
||||
|
||||
class Shortener:
|
||||
"""
|
||||
Shortener Object
|
||||
"""
|
||||
|
||||
def __init__(self, session, configuration: dict) -> typing.NoReturn:
|
||||
"""
|
||||
Initialize the Shortener object.
|
||||
|
||||
:param configuration: The shortenit configuration
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.session = session
|
||||
self.configuration = configuration
|
||||
self.length = 32
|
||||
self.check_duplicate = False
|
||||
self.upper_case = False
|
||||
self.init()
|
||||
|
||||
def init(self) -> typing.NoReturn:
|
||||
"""
|
||||
Initialize the shortener from the configuration.
|
||||
"""
|
||||
length = self.configuration.get("id_length", 32)
|
||||
if length > 32 or length <= 8:
|
||||
self.length = 32
|
||||
self.logger.warn(
|
||||
"ID length provided is not between '8' and '32', reverting to default of '32'"
|
||||
)
|
||||
else:
|
||||
self.length = length
|
||||
self.check_duplicate = self.configuration.get("check_duplicate_id", False)
|
||||
self.upper_case = self.configuration.get("id_upper_case", False)
|
||||
|
||||
def generate_short_uuid(self) -> str:
|
||||
"""
|
||||
Method to generate UUID.
|
||||
|
||||
:returns: A UUID.
|
||||
"""
|
||||
_uuid = uuid.uuid1().hex
|
||||
if self.upper_case:
|
||||
return _uuid.upper()[-self.length :]
|
||||
return _uuid.lower()[-self.length :]
|
||||
|
||||
def check_uuid(self, short_uuid: str) -> bool:
|
||||
"""
|
||||
Method to check the short UUID against the database.
|
||||
|
||||
:returns: True if UUID exists in the database.
|
||||
"""
|
||||
try:
|
||||
_ = self.session.query(objects.Pointer).filter_by(data=short_uuid).one()
|
||||
except exc.NoResultFound:
|
||||
self.logger.debug(
|
||||
"Generated short uuid '%s' was not found in the database.", short_uuid
|
||||
)
|
||||
return False
|
||||
|
||||
self.logger.warn("Generated short uuid '%s' was found in the database.")
|
||||
return True
|
||||
|
||||
def generate_uuid(self) -> str:
|
||||
"""
|
||||
Method to generate a UUID.
|
||||
This method will generate a UUID and check if it already exists.
|
||||
|
||||
:returns: A UUID.
|
||||
"""
|
||||
short_uuid = self.generate_short_uuid()
|
||||
if self.check_duplicate:
|
||||
counter = 0
|
||||
while self.check_uuid(short_uuid=short_uuid):
|
||||
if counter > 10:
|
||||
self.logger.err(
|
||||
"Cannot generate a new unique ID,"
|
||||
"try to configure a longer ID length."
|
||||
)
|
||||
return None
|
||||
short_uuid = self.generate_short_uuid()
|
||||
counter += 1
|
||||
self.logger.debug("Returning ID: '%s'", short_uuid)
|
||||
return short_uuid
|
67
shortenit/pointer.py
Normal file
67
shortenit/pointer.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import time
|
||||
import logging
|
||||
|
||||
from .counter import Counter
|
||||
from cloudant.document import Document
|
||||
|
||||
CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
|
||||
|
||||
class Pointer:
|
||||
def __init__(self, pointers_db: object,
|
||||
counter: Counter = None) -> None:
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.pointers_db = pointers_db
|
||||
self.counter = counter
|
||||
self.identifier = None
|
||||
self.data_hash = None
|
||||
self.ttl = None
|
||||
self.timestamp = time.time()
|
||||
|
||||
def generate_pointer(self, data_hash: str, ttl: time.time):
|
||||
self.logger.debug("Generating new counter...")
|
||||
counter = self.counter.get_counter()
|
||||
self.logger.debug("Encoding the counter into an ID")
|
||||
self.identifier = Pointer.encode(counter)
|
||||
self.logger.debug("Encoded counter is %s", self.identifier)
|
||||
with Document(self.pointers_db, self.identifier) as pointer:
|
||||
pointer['value'] = data_hash
|
||||
pointer['ttl'] = ttl
|
||||
pointer['timestamp'] = self.timestamp
|
||||
self.data_hash = data_hash
|
||||
self.ttl = ttl
|
||||
return self
|
||||
|
||||
def get_pointer(self, identifier: str):
|
||||
with Document(self.pointers_db, identifier) as pointer:
|
||||
try:
|
||||
self.identifier = pointer['_id']
|
||||
self.data_hash = pointer['value']
|
||||
self.ttl = pointer['ttl']
|
||||
self.timestamp = pointer['timestamp']
|
||||
return self
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def encode(counter):
|
||||
sign = '-' if counter < 0 else ''
|
||||
counter = abs(counter)
|
||||
result = ''
|
||||
while counter > 0:
|
||||
counter, remainder = divmod(counter, len(CHARS))
|
||||
result = CHARS[remainder]+result
|
||||
|
||||
return sign+result
|
||||
|
||||
@staticmethod
|
||||
def decode(counter):
|
||||
return int(counter, len(CHARS))
|
||||
|
||||
@staticmethod
|
||||
def padding(counter: str, count: int=6):
|
||||
if len(counter) < count:
|
||||
pad = '0' * (count - len(counter))
|
||||
return f"{pad}{counter}"
|
||||
return f"{counter}"
|
121
shortenit/templates/index.html
Normal file
121
shortenit/templates/index.html
Normal file
|
@ -0,0 +1,121 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="The personal URL Shortener">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>ShortenIt</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
|
||||
</head>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var href = window.location.href;
|
||||
|
||||
$(function() {
|
||||
$('#submitButton').click(function() {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/shortenit",
|
||||
data: JSON.stringify({'url' : $('#url-input').val(),
|
||||
'timestamp': Date.now()}),
|
||||
success: returnSuccess,
|
||||
error: returnFailure,
|
||||
dataType: 'json',
|
||||
contentType: "application/json",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function returnSuccess(data, textStatus, jqXHR) {
|
||||
var url = href.concat(data.url);
|
||||
console.log(href)
|
||||
if(data.url) {
|
||||
document.getElementById("url-result").value = url;
|
||||
} else {
|
||||
document.getElementById("url-result").value = "The URL was too short and somehow got lost on the way, please try generating a new one.";
|
||||
}
|
||||
}
|
||||
|
||||
function returnFailure(data, textStatus, jqXHR) {
|
||||
document.getElementById("url-result").value = "Please enter a valid URL!";
|
||||
}
|
||||
|
||||
window.onload=function() {
|
||||
console.log(ClipboardJS.isSupported())
|
||||
var clipboard = new ClipboardJS('.btn', {
|
||||
container: document.getElementById('copy-button')
|
||||
});
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
console.info('Action:', e.action);
|
||||
console.info('Text:', e.text);
|
||||
console.info('Trigger:', e.trigger);
|
||||
});
|
||||
|
||||
clipboard.on('error', function(e) {
|
||||
console.error('Action:', e.action);
|
||||
console.error('Trigger:', e.trigger);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h1 class="mt-5">Shorten It!</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 form-group">
|
||||
<div class="form-row">
|
||||
<div class="col-9">
|
||||
<input type="text" class="form-control" name="url" id="url-input" placeholder="https://www.duckduckgo.com" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<button id="submitButton" type="button" class="btn btn-outline-primary">Shorten URL</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <a href="#" id="url-result">Enter URL</a> -->
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 form-group">
|
||||
<div class="form-row">
|
||||
<div class="col-9">
|
||||
<input type="text" id="url-result" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div>
|
||||
<button class="btn btn-outline-primary" data-clipboard-target="#url-result" data-clipboard-action="copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap core JavaScript -->
|
||||
</body>
|
||||
|
||||
</html>
|
100
shortenit/web.py
100
shortenit/web.py
|
@ -1,12 +1,14 @@
|
|||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlunparse
|
||||
|
||||
import trafaret
|
||||
from flask import Flask, abort, redirect, request, send_from_directory
|
||||
from flask_cors import CORS
|
||||
|
||||
from .common import check_file
|
||||
from pathlib import Path
|
||||
from flask import Flask
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import redirect
|
||||
from flask import abort
|
||||
|
||||
|
||||
class Web:
|
||||
|
@ -23,56 +25,28 @@ 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()
|
||||
if server_config and server_config.get("cors", False):
|
||||
self.logger.debug("Enabling CORS...")
|
||||
CORS(self.app)
|
||||
|
||||
def setup_routes(self):
|
||||
if self.handler.configuration.get("Server", None)["enable_ui"]:
|
||||
self.app.add_url_rule(
|
||||
"/", "/", self.handler.index, methods=["GET"], defaults={"path": ""}
|
||||
)
|
||||
self.app.add_url_rule("/<path:path>", "/", self.handler.index, methods=["GET"])
|
||||
self.app.add_url_rule(
|
||||
"/static/css/<path:path>", "css", self.handler.css, methods=["GET"]
|
||||
)
|
||||
self.app.add_url_rule(
|
||||
"/static/js/<path:path>", "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/<identifier>", "redirect", self.handler.short_redirect, methods=["GET"]
|
||||
)
|
||||
self.app.add_url_rule('/', '/', self.handler.index,
|
||||
methods=['GET'])
|
||||
self.app.add_url_rule('/shortenit', '/shortenit', self.handler.shortenit,
|
||||
methods=['POST'])
|
||||
self.app.add_url_rule('/<identifier>', '/identifier', self.handler.short_redirect,
|
||||
methods=['GET'])
|
||||
|
||||
|
||||
class SiteHandler:
|
||||
def __init__(self, configuration, database, shorten_url, lenghten_url):
|
||||
def __init__(self, database, shorten_url, lenghten_url):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.configuration = configuration
|
||||
self.database = database
|
||||
self.shorten_url = shorten_url
|
||||
self.lenghten_url = lenghten_url
|
||||
self.shortenit_load_format = trafaret.Dict({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()["hostname"]
|
||||
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()
|
||||
if (port == "443" and scheme == "https") or (port == "80" and scheme == "http"):
|
||||
return urlunparse((scheme, f"{host}", f"/r/{stub}", "", "", ""))
|
||||
return urlunparse((scheme, f"{host}:{port}", f"/r/{stub}", "", "", ""))
|
||||
self.shortenit_load_format = trafaret.Dict({
|
||||
trafaret.Key('url'): trafaret.URL,
|
||||
trafaret.Key('timestamp'): trafaret.Int
|
||||
})
|
||||
|
||||
def shortenit(self):
|
||||
data = request.get_json()
|
||||
|
@ -80,16 +54,12 @@ class SiteHandler:
|
|||
data = self.shortenit_load_format(data)
|
||||
except Exception as e:
|
||||
self.logger.error(e)
|
||||
return {"msg" : "Invalid URL format"}, 400
|
||||
return {}, 400
|
||||
self.logger.error(e)
|
||||
abort(400)
|
||||
try:
|
||||
stub = self.shorten_url(
|
||||
self.configuration.get("Shortener", None),
|
||||
self.database,
|
||||
data["url"],
|
||||
)
|
||||
short_url = self._get_url(stub)
|
||||
short_url = self.shorten_url(
|
||||
self.database, data['url'], data['timestamp'])
|
||||
except KeyError as e:
|
||||
self.logger.error(e)
|
||||
abort(400)
|
||||
|
@ -104,29 +74,5 @@ class SiteHandler:
|
|||
abort(404)
|
||||
return redirect(url)
|
||||
|
||||
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)
|
||||
def index(self):
|
||||
return render_template('index.html')
|
||||
|
|
Loading…
Reference in a new issue