"""Guarda dos links encurtados. Os links ficam num dicionário na memória do processo: simples e sem banco, mas somem toda vez que o app reinicia ou é publicado de novo. Para guardar de vez, troque este arquivo por consultas a um Postgres. """ import re import secrets from datetime import datetime, timezone from urllib.parse import urlsplit ALFABETO = "abcdefghijklmnopqrstuvwxyz0123456789" TAMANHO_CODIGO = 6 TAMANHO_MAXIMO_URL = 2048 # Teto de links guardados: passou disso, o mais antigo sai (protege a memória). MAXIMO_DE_LINKS = 1000 LINKS_NA_LISTA = 20 # Caminhos do próprio app que nunca podem virar código. RESERVADOS = {"api", "docs"} FORMATO_CODIGO = re.compile(r"^[a-z0-9]{6}$") # Dicionário do Python mantém a ordem de inserção: o primeiro é o mais antigo. _links: dict[str, dict] = {} class ErroDeValidacao(ValueError): pass def _gerar_codigo() -> str: while True: codigo = "".join(secrets.choice(ALFABETO) for _ in range(TAMANHO_CODIGO)) if codigo not in _links and codigo not in RESERVADOS: return codigo def _validar_url(valor) -> str: if not isinstance(valor, str) or not valor.strip(): raise ErroDeValidacao('Envie o campo "url" com o link que você quer encurtar.') texto = valor.strip() if len(texto) > TAMANHO_MAXIMO_URL: raise ErroDeValidacao(f"Esse link é comprido demais (máximo de {TAMANHO_MAXIMO_URL} caracteres).") try: partes = urlsplit(texto) except ValueError: partes = None if not partes or partes.scheme not in ("http", "https") or not partes.hostname or any(c.isspace() for c in texto): raise ErroDeValidacao("Esse não parece um link válido. Use um endereço que comece com http:// ou https://.") return texto def criar_link(valor) -> dict: link = { "codigo": _gerar_codigo(), "url": _validar_url(valor), "cliques": 0, "criadoEm": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), } _links[link["codigo"]] = link if len(_links) > MAXIMO_DE_LINKS: del _links[next(iter(_links))] return link def listar_links() -> list[dict]: """Os últimos links criados, do mais novo para o mais antigo.""" return list(reversed(list(_links.values())[-LINKS_NA_LISTA:])) def registrar_clique(codigo: str) -> dict | None: """Soma um clique e devolve o link, ou None se o código não existe.""" link = _links.get(codigo) if link is None: return None link["cliques"] += 1 return link