Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b2ca72a809 | ||
6060bd6b07 | |||
d8ccfeeb14 | |||
|
3fa367602f | ||
|
e9d2b2b23e | ||
|
7ce20357be | ||
|
28b68e9a74 | ||
|
3f42b5bb57 | ||
|
887aab2960 | ||
|
db07547ecb | ||
|
126af19e87 | ||
|
71a4ad4a65 | ||
|
0c44ad5c6b | ||
|
6853e02b1f | ||
|
5b37f5759c | ||
|
54945189b6 |
20 changed files with 402 additions and 184 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
.eggs/
|
||||
*.egg-info
|
||||
__pycache__/
|
||||
.mypy*/
|
||||
.vscode/
|
||||
|
|
25
LICENSE
Normal file
25
LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
|||
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.
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Shortenit
|
||||
|
||||
Shortenit is a tool to shorten urls.
|
||||
|
||||
**NOTE**: This is a very early draft project. Contributions are welcome.
|
||||
|
||||
## Running
|
||||
|
||||
To run `shortenit`, first we need to have a running database. Shortenit uses [CouchDB](https://couchdb.apache.org/) as a database. CouchDB can run as a docker container.
|
||||
|
||||
```text
|
||||
$ docker run -p 5984:5984 -d couchdb
|
||||
```
|
||||
|
||||
At this point, visit the local instance link [http://localhost:5984](http://localhost:5984/) and create the user credentials configured in [config/config.yaml](config/config.yaml).
|
||||
|
||||
Once the database is up and running and the credentials have been created, shortenit can be ran.
|
||||
|
||||
```text
|
||||
$ pip install -e .
|
||||
$ shortenit
|
||||
```
|
|
@ -1,6 +0,0 @@
|
|||
shortenit
|
||||
=========
|
||||
|
||||
shortenit is a tool to shorten urls.
|
||||
|
||||
NOTE: This is a very early draft project. Contributions are welcome.
|
10
TODO
Normal file
10
TODO
Normal file
|
@ -0,0 +1,10 @@
|
|||
This list is incomplete, these are a few of the things that need to be done.
|
||||
|
||||
TODO:
|
||||
* Documentation/Code comments
|
||||
* Proper type-setting across the board
|
||||
* PEP8
|
||||
* Refactor code
|
||||
* Dockerize
|
||||
* favicon.ico is being injected into the database (need fix)
|
||||
* Fix YAML warning on startup.
|
|
@ -5,3 +5,10 @@ CouchDB:
|
|||
username: root
|
||||
password: root
|
||||
url: http://localhost:5984
|
||||
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.
|
||||
check_duplicate_id: False
|
||||
id_length: 32
|
||||
id_upper_case: False
|
|
@ -1 +1,4 @@
|
|||
-r requirements.txt
|
||||
setuptools-git
|
||||
setuptools-git-version
|
||||
mypy
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
pyyaml
|
||||
cloudant
|
||||
aiohttp
|
||||
aiohttp-jinja2
|
||||
flask
|
||||
trafaret
|
||||
|
|
5
setup.py
5
setup.py
|
@ -4,7 +4,7 @@ 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:
|
||||
with open(str(here.joinpath('README.md')), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
with open(str(here.joinpath('requirements/requirements.txt')),
|
||||
|
@ -16,7 +16,7 @@ description = 'Shortenit will shorten that url for you'
|
|||
setup(
|
||||
name='shortenit',
|
||||
exclude_package_data={'': ['.gitignore', 'requirements/', 'setup.py']},
|
||||
version_format='{tag}_{gitsha}',
|
||||
version_format='{gitsha}',
|
||||
setup_requires=['setuptools-git', 'setuptools-git-version'],
|
||||
license='BSD',
|
||||
author='Elia El Lazkani',
|
||||
|
@ -40,6 +40,7 @@ setup(
|
|||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Information Technology',
|
||||
'Intended Audience :: System Administrators'
|
||||
|
|
|
@ -2,21 +2,44 @@ 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):
|
||||
def get_config(self) -> str:
|
||||
"""
|
||||
Get the configuration saved in the configuration file.
|
||||
|
||||
:returns: The configuration saved in the configuration file.
|
||||
"""
|
||||
if not self.config:
|
||||
_config = self.load_config()
|
||||
self.config = _config
|
||||
return self.config
|
||||
|
||||
def load_config(self):
|
||||
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.load(f)
|
||||
config = yaml.safe_load(f)
|
||||
if self.validate_config(config):
|
||||
return config
|
||||
|
||||
def validate_config(self, config):
|
||||
def validate_config(self, config: str) -> bool:
|
||||
"""
|
||||
Validate the configuration.
|
||||
|
||||
:returns: Configuration validation status.
|
||||
"""
|
||||
return True
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
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
|
|
@ -1,3 +1,4 @@
|
|||
import typing
|
||||
import time
|
||||
import logging
|
||||
|
||||
|
@ -6,9 +7,19 @@ from cloudant.document import Document
|
|||
|
||||
|
||||
class Data:
|
||||
"""
|
||||
Data object.
|
||||
"""
|
||||
def __init__(self, data_db: object,
|
||||
identifier: str = None,
|
||||
data: str = None):
|
||||
data: str = None) -> typing.NoReturn:
|
||||
"""
|
||||
Initialize the Data object.
|
||||
|
||||
:param data_db: The Data database object.
|
||||
:param identifier: A uniquely generated ID identifying the data object.
|
||||
:param data: The data to save.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.data_db = data_db
|
||||
self.identifier = identifier
|
||||
|
@ -18,11 +29,19 @@ class Data:
|
|||
self.data_found = None
|
||||
self.populate()
|
||||
|
||||
def generate_identifier(self):
|
||||
def generate_identifier(self) -> typing.NoReturn:
|
||||
"""
|
||||
Method to generate and save a new unique ID as the Data object identifier.
|
||||
"""
|
||||
hash_object = sha256(self.data.encode('utf-8'))
|
||||
self.identifier = hash_object.hexdigest()
|
||||
|
||||
def populate(self, pointer: str = None):
|
||||
def populate(self, pointer: str = None) -> typing.NoReturn:
|
||||
"""
|
||||
Method to populate the Data object fields with proper data and save it in the database.
|
||||
|
||||
:param pointer: The unique ID of the pointer object to save with the data.
|
||||
"""
|
||||
if self.identifier:
|
||||
self.logger.debug("The identifier is set, retrieving data...")
|
||||
self.get_data()
|
||||
|
@ -37,7 +56,10 @@ class Data:
|
|||
"creating...")
|
||||
self.set_data(pointer)
|
||||
|
||||
def get_data(self):
|
||||
def get_data(self) -> typing.NoReturn:
|
||||
"""
|
||||
Method to retrieve the Data ojbect from the database.
|
||||
"""
|
||||
with Document(self.data_db, self.identifier) as data:
|
||||
try:
|
||||
self.data = data['value']
|
||||
|
@ -47,7 +69,10 @@ class Data:
|
|||
except KeyError:
|
||||
self.data_found = False
|
||||
|
||||
def set_data(self, pointer):
|
||||
def set_data(self, pointer: str) -> typing.NoReturn:
|
||||
"""
|
||||
Method to save Data object to the database.
|
||||
"""
|
||||
with Document(self.data_db, self.identifier) as data:
|
||||
data['value'] = self.data
|
||||
data['timestamp'] = self.timestamp
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
import typing
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from cloudant.client import CouchDB
|
||||
|
||||
from shortenit.exceptions import DBConnectionFailed
|
||||
|
||||
class DB:
|
||||
def __init__(self, config: dict) -> None:
|
||||
"""
|
||||
Database object class
|
||||
"""
|
||||
def __init__(self, config: dict) -> typing.NoReturn:
|
||||
"""
|
||||
Initialize the Database object.
|
||||
|
||||
:param config: The Database configuration.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.username = config['username']
|
||||
self.password = config['password']
|
||||
|
@ -12,17 +23,12 @@ class DB:
|
|||
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.")
|
||||
|
||||
def initialize_shortenit(self) -> typing.NoReturn:
|
||||
"""
|
||||
Method to initialize the database for shortenit.
|
||||
This will check if all the needed tables already exist in the database.
|
||||
Otherwise, it will create the database tables.
|
||||
"""
|
||||
try:
|
||||
self.data_db = self.client['data']
|
||||
except KeyError:
|
||||
|
@ -45,13 +51,26 @@ class DB:
|
|||
|
||||
def __enter__(self) -> CouchDB:
|
||||
"""
|
||||
Method used when entering the database context.
|
||||
|
||||
:returns: The CouchDB object.
|
||||
"""
|
||||
self.client = CouchDB(self.username, self.password,
|
||||
url=self.url, connect=True)
|
||||
try:
|
||||
self.client = CouchDB(self.username, self.password,
|
||||
url=self.url, connect=True)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
self.logger.fatal("Failed to connect to database, is it on?")
|
||||
self.logger.fatal("%s", e)
|
||||
raise DBConnectionFailed
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.fatal("Failed to authenticate to database.")
|
||||
self.logger.fatal("%s", e)
|
||||
raise DBConnectionFailed
|
||||
self.session = self.client.session()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
def __exit__(self, *args) -> typing.NoReturn:
|
||||
"""
|
||||
Method used when exiting the database context.
|
||||
"""
|
||||
self.client.disconnect()
|
||||
|
|
7
shortenit/exceptions.py
Normal file
7
shortenit/exceptions.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import sys
|
||||
|
||||
class DBConnectionFailed(Exception):
|
||||
"""
|
||||
DBConnectionFailed class exception.
|
||||
"""
|
||||
pass
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import typing
|
||||
import yaml
|
||||
import logging.config
|
||||
from .common import check_file
|
||||
|
@ -6,7 +7,7 @@ from .common import check_file
|
|||
|
||||
def setup_logging(default_path: str = None,
|
||||
default_level: int = logging.ERROR,
|
||||
env_key: str = 'LOG_CFG') -> None:
|
||||
env_key: str = 'LOG_CFG') -> typing.NoReturn:
|
||||
"""
|
||||
Method that sets the logging system up.
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import typing
|
||||
import argparse
|
||||
import logging
|
||||
import pathlib
|
||||
|
@ -6,13 +8,14 @@ import asyncio
|
|||
|
||||
import time
|
||||
|
||||
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
|
||||
from shortenit.data import Data
|
||||
from shortenit.pointer import Pointer
|
||||
from shortenit.config import Config
|
||||
from shortenit.shortener import Shortener
|
||||
from shortenit.db import DB
|
||||
from shortenit.logger import setup_logging
|
||||
from shortenit.web import Web, SiteHandler
|
||||
from shortenit.exceptions import DBConnectionFailed
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).parent.parent
|
||||
CONFIGURATION = f'{PROJECT_ROOT}/config/config.yaml'
|
||||
|
@ -21,41 +24,47 @@ CONFIGURATION = f'{PROJECT_ROOT}/config/config.yaml'
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def main() -> typing.NoReturn:
|
||||
"""
|
||||
Main method
|
||||
"""
|
||||
parser = argument_parse()
|
||||
args = parser.parse_args()
|
||||
debug = True if args.verbose > 0 else False
|
||||
verbosity_level = verbosity(args.verbose)
|
||||
setup_logging(args.logger, verbosity_level)
|
||||
config = Config(CONFIGURATION).get_config()
|
||||
db_config = config.get('CouchDB', None)
|
||||
server_config = config.get('Server', None)
|
||||
if db_config:
|
||||
with DB(db_config) as db:
|
||||
db.initialize_shortenit()
|
||||
try:
|
||||
with DB(db_config) as db:
|
||||
db.initialize_shortenit()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
handler = SiteHandler(db, shorten_url, lenghten_url)
|
||||
web = Web(loop, handler)
|
||||
web.host = server_config.get('host', None)
|
||||
web.port = server_config.get('port', None)
|
||||
web.start_up()
|
||||
handler = SiteHandler(config, db, shorten_url, lenghten_url)
|
||||
web = Web(handler, debug=debug)
|
||||
web.host = server_config.get('host', None)
|
||||
web.port = server_config.get('port', None)
|
||||
web.start_up()
|
||||
except DBConnectionFailed as e:
|
||||
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_url(configuration: dict, database: DB,
|
||||
data: str, ttl):
|
||||
shortener = Shortener(database.pointers_db,
|
||||
configuration.get('Shortener', None))
|
||||
identifier = shortener.get_id()
|
||||
if identifier:
|
||||
_data = Data(database.data_db,
|
||||
data=data)
|
||||
_data.populate()
|
||||
pointer = Pointer(database.pointers_db, identifier)
|
||||
pointer.generate_pointer(_data.identifier, ttl)
|
||||
_data.set_data(pointer.identifier)
|
||||
return pointer.identifier
|
||||
return None
|
||||
|
||||
|
||||
def lenghten_url(database: DB, identifier: str):
|
||||
|
@ -100,3 +109,7 @@ def verbosity(verbose: int):
|
|||
return logging.INFO
|
||||
elif verbose > 2:
|
||||
return logging.DEBUG
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -1,29 +1,40 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import typing
|
||||
import logging
|
||||
|
||||
from .counter import Counter
|
||||
from cloudant.document import Document
|
||||
|
||||
CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
|
||||
|
||||
class Pointer:
|
||||
"""
|
||||
Pointer object.
|
||||
"""
|
||||
def __init__(self, pointers_db: object,
|
||||
counter: Counter = None) -> None:
|
||||
identifier: str = None) -> typing.NoReturn:
|
||||
"""
|
||||
Initialize the Pointer object.
|
||||
|
||||
:param pointers_db: The Pointer database object.
|
||||
:param identifier: A uniquely generated ID identifying the pointer object.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.pointers_db = pointers_db
|
||||
self.counter = counter
|
||||
self.identifier = None
|
||||
self.identifier = identifier
|
||||
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)
|
||||
def generate_pointer(self, data_hash: str, ttl: time.time) -> Pointer:
|
||||
"""
|
||||
Generates a pointer object and saves it into the database.
|
||||
|
||||
:param data_hash: A uniquely generated ID identifying the data object.
|
||||
:param ttl: The "Time to Live" of the pointer.
|
||||
:returns: The Pointer object.
|
||||
"""
|
||||
self.logger.debug("identifier is %s", self.identifier)
|
||||
with Document(self.pointers_db, self.identifier) as pointer:
|
||||
pointer['value'] = data_hash
|
||||
pointer['ttl'] = ttl
|
||||
|
@ -32,7 +43,13 @@ class Pointer:
|
|||
self.ttl = ttl
|
||||
return self
|
||||
|
||||
def get_pointer(self, identifier: str):
|
||||
def get_pointer(self, identifier: str) -> Pointer:
|
||||
"""
|
||||
Retrieve a pointer object from the database.
|
||||
|
||||
:param identifier: A uniquely generated ID identifying the Pointer object.
|
||||
:returns: The Pointer object requested.
|
||||
"""
|
||||
with Document(self.pointers_db, identifier) as pointer:
|
||||
try:
|
||||
self.identifier = pointer['_id']
|
||||
|
@ -43,25 +60,3 @@ class Pointer:
|
|||
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}"
|
||||
|
|
88
shortenit/shortener.py
Normal file
88
shortenit/shortener.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
import uuid
|
||||
import typing
|
||||
import logging
|
||||
|
||||
from cloudant.document import Document
|
||||
|
||||
|
||||
class Shortener:
|
||||
"""
|
||||
Shortener object
|
||||
"""
|
||||
def __init__(self, pointer_db, configuration: dict) -> typing.NoReturn:
|
||||
"""
|
||||
Initialize the Shortener object.
|
||||
|
||||
:param pointer_db: The Pointer Database object.
|
||||
:param configuration: The shortenit configuration.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.pointer_db = pointer_db
|
||||
self.uuid = None
|
||||
self.length = 32
|
||||
self.check_duplicate = False
|
||||
self.upper_case = False
|
||||
self.configuration = configuration
|
||||
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 <= 0:
|
||||
self.length = 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:
|
||||
"""
|
||||
Generate a short UUID in Hex format.
|
||||
|
||||
:returns: A short UUID in Hex format.
|
||||
"""
|
||||
short_uuid = uuid.uuid1().hex
|
||||
if self.upper_case:
|
||||
return short_uuid.upper()[0:self.length]
|
||||
return short_uuid.lower()[0:self.length]
|
||||
|
||||
def check_uuid(self, short_uuid) -> bool:
|
||||
"""
|
||||
Check a short UUID against the database.
|
||||
|
||||
:returns: Whether the UUID exists in the database or not.
|
||||
"""
|
||||
with Document(self.pointer_db, 'pointer') as pointer:
|
||||
self.logger.debug("Pointer: %s", pointer)
|
||||
try:
|
||||
self.uuid = pointer[short_uuid]
|
||||
except KeyError:
|
||||
self.logger.info("Generated short uuid '%s'"
|
||||
"was not found in database",
|
||||
short_uuid)
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_id(self) -> str:
|
||||
"""
|
||||
Method to get a UUID.
|
||||
This method will generate a UUID and checks if it already exists in the database.
|
||||
|
||||
:returns: A UUID.
|
||||
"""
|
||||
short_uuid = self.generate_short_uuid()
|
||||
if self.check_duplicate:
|
||||
counter = 0
|
||||
while self.check_uuid(short_uuid):
|
||||
if counter > 10:
|
||||
self.logger.err("Cannot generate 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
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
<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">
|
||||
|
@ -33,6 +34,7 @@
|
|||
data: JSON.stringify({'url' : $('#url-input').val(),
|
||||
'timestamp': Date.now()}),
|
||||
success: returnSuccess,
|
||||
error: returnFailure,
|
||||
dataType: 'json',
|
||||
contentType: "application/json",
|
||||
});
|
||||
|
@ -41,28 +43,36 @@
|
|||
|
||||
function returnSuccess(data, textStatus, jqXHR) {
|
||||
var url = href.concat(data.url);
|
||||
console.log(window.location.href)
|
||||
console.log(href)
|
||||
if(data.url) {
|
||||
document.getElementById("url-result").value = url;
|
||||
} else {
|
||||
document.getElementById("url-result").value = "Please enter a valid URL!";
|
||||
document.getElementById("url-result").value = "The URL was too short and somehow got lost on the way, please try generating a new one.";
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
/* Get the text field */
|
||||
var copyText = document.querySelector("#url-result");
|
||||
function returnFailure(data, textStatus, jqXHR) {
|
||||
document.getElementById("url-result").value = "Please enter a valid URL!";
|
||||
}
|
||||
|
||||
/* Select the text field */
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
|
||||
window.onload=function() {
|
||||
console.log(ClipboardJS.isSupported())
|
||||
var clipboard = new ClipboardJS('.btn', {
|
||||
container: document.getElementById('copy-button')
|
||||
});
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/* Alert the copied text */
|
||||
alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<body>
|
||||
|
@ -98,7 +108,8 @@
|
|||
<input type="text" id="url-result" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<button class="btn btn-outline-primary" type="button" id="copy-button" data-toggle="tooltip" onclick="copyToClipboard()" data-placement="button" title="Copy to Clipboard">Copy</button>
|
||||
<div>
|
||||
<button class="btn btn-outline-primary" data-clipboard-target="#url-result" data-clipboard-action="copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,78 +1,80 @@
|
|||
import os
|
||||
import logging
|
||||
import aiohttp_jinja2
|
||||
import jinja2
|
||||
|
||||
import trafaret as t
|
||||
import trafaret
|
||||
|
||||
from aiohttp import web
|
||||
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:
|
||||
def __init__(self, loop, handler):
|
||||
def __init__(self, handler, debug=False):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.loop = loop
|
||||
self.app = None
|
||||
self.host = None
|
||||
self.port = None
|
||||
self.handler = handler
|
||||
self.router = None
|
||||
self.loader = None
|
||||
self.debug = debug
|
||||
|
||||
def start_up(self):
|
||||
self.loop.run_until_complete(self.init())
|
||||
web.run_app(self.app, host=self.host, port=self.port)
|
||||
self.init()
|
||||
self.app.run(host=self.host, port=self.port, debug=self.debug)
|
||||
|
||||
async def init(self):
|
||||
self.app = web.Application(loop=self.loop)
|
||||
templates = Path(__file__).absolute().parent.parent.joinpath('templates')
|
||||
|
||||
self.loader = jinja2.FileSystemLoader(str(templates))
|
||||
self.logger.debug(str(templates))
|
||||
aiohttp_jinja2.setup(self.app,
|
||||
loader=self.loader)
|
||||
def init(self):
|
||||
self.app = Flask(__name__)
|
||||
self.setup_routes()
|
||||
|
||||
def setup_routes(self):
|
||||
self.router = self.app.router
|
||||
self.router.add_get('/', self.handler.index,
|
||||
name='index')
|
||||
self.router.add_post('/shortenit', self.handler.shortenit,
|
||||
name='shortenit')
|
||||
self.router.add_get('/{identifier}', self.handler.redirect,
|
||||
name='redirect')
|
||||
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, database, shorten_url, lenghten_url):
|
||||
def __init__(self, configuration, 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 = t.Dict({
|
||||
t.Key('url'): t.URL,
|
||||
t.Key('timestamp'): t.Int
|
||||
self.shortenit_load_format = trafaret.Dict({
|
||||
trafaret.Key('url'): trafaret.URL,
|
||||
trafaret.Key('timestamp'): trafaret.Int
|
||||
})
|
||||
|
||||
async def shortenit(self, request):
|
||||
data = await request.json()
|
||||
self.logger.debug(data)
|
||||
def shortenit(self):
|
||||
data = request.get_json()
|
||||
try:
|
||||
data = self.shortenit_load_format(data)
|
||||
except t.DataError:
|
||||
raise web.HTTPBadRequest('URL is not valid')
|
||||
short_url = self.shorten_url(self.database, data['url'], data['timestamp'])
|
||||
except Exception as e:
|
||||
self.logger.error(e)
|
||||
return {}, 400
|
||||
self.logger.error(e)
|
||||
abort(400)
|
||||
try:
|
||||
short_url = self.shorten_url(
|
||||
self.configuration, self.database,
|
||||
data['url'], data['timestamp'])
|
||||
except KeyError as e:
|
||||
self.logger.error(e)
|
||||
abort(400)
|
||||
self.logger.debug(short_url)
|
||||
return web.json_response({"url": short_url})
|
||||
return {"url": short_url}
|
||||
|
||||
async def redirect(self, request):
|
||||
identifier = request.match_info['identifier']
|
||||
def short_redirect(self, identifier):
|
||||
url = self.lenghten_url(self.database, identifier)
|
||||
self.logger.debug("The URL is...")
|
||||
self.logger.debug(url)
|
||||
if not url:
|
||||
raise web.HTTPNotFound()
|
||||
return web.HTTPFound(location=url)
|
||||
abort(404)
|
||||
return redirect(url)
|
||||
|
||||
@aiohttp_jinja2.template('index.html')
|
||||
async def index(self, request):
|
||||
return {}
|
||||
def index(self):
|
||||
return render_template('index.html')
|
||||
|
|
Loading…
Reference in a new issue