Implementa sistema de IDs curtos para pastes, substituindo hashes SHA-256. Atualiza a estrutura do banco de dados e a lógica de inserção para suportar a detecção de duplicatas. O README e a documentação de migração foram atualizados para refletir essas mudanças.

This commit is contained in:
Luiz Costa
2025-08-29 08:57:44 -03:00
parent 9abff0338b
commit 18c4b0613f
8 changed files with 334 additions and 47 deletions

View File

@@ -1,24 +1,39 @@
# Migração para SQLite3
# Migração para SQLite3 com IDs Curtos
Este documento descreve como migrar o YASUC de BoltDB para SQLite3.
Este documento descreve como migrar o YASUC de BoltDB para SQLite3 com o novo sistema de IDs curtos.
## Por que migrar para SQLite3?
## Por que migrar para SQLite3 com IDs Curtos?
- **URLs mais amigáveis**: IDs de 7 caracteres em vez de hashes SHA-256 de 64 caracteres
- **Melhor compatibilidade**: SQLite3 é mais amplamente suportado
- **Ferramentas de administração**: Mais ferramentas disponíveis para gerenciar dados
- **Consultas SQL**: Possibilidade de fazer consultas complexas
- **Backup mais simples**: Arquivo único mais fácil de fazer backup
- **Detecção de duplicatas**: Pastes idênticos recebem o mesmo ID curto
## Estrutura do Banco SQLite3
```sql
CREATE TABLE pastes (
hash TEXT PRIMARY KEY, -- Hash SHA-256 do conteúdo
content TEXT NOT NULL, -- Conteúdo do paste
created_at INTEGER NOT NULL -- Timestamp de criação
short_id TEXT PRIMARY KEY, -- ID curto único (7 caracteres)
content TEXT NOT NULL, -- Conteúdo do paste
content_hash TEXT NOT NULL, -- Hash SHA-256 para detecção de duplicatas
created_at INTEGER NOT NULL -- Timestamp de criação
);
```
## Sistema de IDs Curtos
### Características:
- **7 caracteres**: Combinação de letras maiúsculas, minúsculas e números (a-z, A-Z, 0-9)
- **62^7 possibilidades**: Aproximadamente 3.5 trilhões de combinações únicas
- **Detecção de duplicatas**: Pastes idênticos recebem o mesmo ID
- **Geração aleatória**: IDs são gerados aleatoriamente para evitar previsibilidade
### Exemplo de URLs:
- **Antes**: `http://localhost:8080/a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447`
- **Agora**: `http://localhost:8080/Ly34kx3`
## Passos para Migração
### 1. Backup dos Dados Atuais
@@ -45,6 +60,8 @@ Se você quiser migrar dados existentes do BoltDB para SQLite3:
go run cmd/migrate/main.go -bolt data/yasuc.db -sqlite data/yasuc.sqlite3
```
**Nota**: Durante a migração, cada paste do BoltDB receberá um novo ID curto. URLs antigas não funcionarão mais.
### 4. Testar a Aplicação
```bash
@@ -73,6 +90,12 @@ sqlite3 data/yasuc.sqlite3 ".schema pastes"
sqlite3 data/yasuc.sqlite3 "SELECT COUNT(*) FROM pastes;"
```
### Verificar IDs curtos gerados:
```bash
sqlite3 data/yasuc.sqlite3 "SELECT short_id, substr(content, 1, 50) FROM pastes LIMIT 5;"
```
## Rollback (se necessário)
Se algo der errado, você pode voltar ao BoltDB:
@@ -81,27 +104,31 @@ Se algo der errado, você pode voltar ao BoltDB:
2. Reverter o código para a versão anterior
3. Reconstruir o container
## Vantagens do SQLite3
## Vantagens do SQLite3 com IDs Curtos
1. **Consultas SQL**: Você pode fazer consultas como:
1. **URLs mais amigáveis**: Fáceis de compartilhar e digitar
2. **Consultas SQL**: Você pode fazer consultas como:
```sql
-- Encontrar pastes criados hoje
SELECT * FROM pastes WHERE date(created_at, 'unixepoch') = date('now');
-- Encontrar pastes maiores que 1KB
SELECT hash, length(content) as size FROM pastes WHERE length(content) > 1024;
SELECT short_id, length(content) as size FROM pastes WHERE length(content) > 1024;
-- Verificar duplicatas
SELECT content_hash, COUNT(*) as count FROM pastes GROUP BY content_hash HAVING count > 1;
```
2. **Backup mais simples**: Um único arquivo
3. **Ferramentas de administração**: SQLite Browser, DBeaver, etc.
4. **Melhor performance**: Para consultas complexas
3. **Backup mais simples**: Um único arquivo
4. **Ferramentas de administração**: SQLite Browser, DBeaver, etc.
5. **Melhor performance**: Para consultas complexas
## Notas Importantes
- O hash SHA-256 continua sendo usado como identificador único
- A funcionalidade da API permanece a mesma
- URLs dos pastes existentes continuam funcionando
- O tamanho máximo de paste (4MB) permanece inalterado
- **URLs antigas não funcionam**: Pastes migrados terão novos IDs curtos
- **Detecção de duplicatas**: Pastes idênticos recebem o mesmo ID
- **Tamanho máximo**: O tamanho máximo de paste (4MB) permanece inalterado
- **Compatibilidade**: A API permanece a mesma, apenas os IDs mudaram
## Troubleshooting
@@ -113,6 +140,11 @@ Se algo der errado, você pode voltar ao BoltDB:
- Execute o script de migração novamente
- Verifique se o arquivo SQLite3 foi criado corretamente
### Erro: "não foi possível gerar um ID único"
- Extremamente raro, mas pode acontecer se houver muitas colisões
- O sistema tenta 100 vezes antes de falhar
- Considere aumentar o `maxAttempts` no código se necessário
### Performance lenta
- Considere adicionar índices se necessário
- Verifique se o volume Docker está configurado corretamente

View File

@@ -6,7 +6,7 @@ yet another [sprunge.us](http://sprunge.us/) clone
## Sobre
YASUC é um pastebin de linha de comando que armazena pastes usando SQLite3. Cada paste é identificado pelo hash SHA-256 do seu conteúdo, garantindo que pastes idênticos tenham a mesma URL.
YASUC é um pastebin de linha de comando que armazena pastes usando SQLite3. Cada paste é identificado por um ID curto único de 7 caracteres, tornando as URLs mais amigáveis e fáceis de compartilhar.
## Uso do Servidor
@@ -39,13 +39,20 @@ docker run -d -p 8080:8080 -v /path/to/data:/data yasuc
# Exemplo
echo 'hello world' | curl -F 'sprunge=<-' http://localhost:8080/
# Retorna: http://localhost:8080/a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
# Retorna: http://localhost:8080/Ly34kx3
# Acessar um paste
curl http://localhost:8080/a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
curl http://localhost:8080/Ly34kx3
# Retorna: hello world
```
## Características dos IDs Curtos
- **7 caracteres**: Combinação de letras maiúsculas, minúsculas e números
- **Únicos**: Cada paste tem um ID único
- **Detecção de duplicatas**: Pastes idênticos recebem o mesmo ID
- **Amigáveis**: URLs mais curtas e fáceis de compartilhar
## Migração de BoltDB para SQLite3
Este projeto foi migrado de BoltDB para SQLite3. Para migrar dados existentes:
@@ -58,9 +65,10 @@ Este projeto foi migrado de BoltDB para SQLite3. Para migrar dados existentes:
```sql
CREATE TABLE pastes (
hash TEXT PRIMARY KEY, -- Hash SHA-256 do conteúdo
content TEXT NOT NULL, -- Conteúdo do paste
created_at INTEGER NOT NULL -- Timestamp de criação
short_id TEXT PRIMARY KEY, -- ID curto único (7 caracteres)
content TEXT NOT NULL, -- Conteúdo do paste
content_hash TEXT NOT NULL, -- Hash SHA-256 para detecção de duplicatas
created_at INTEGER NOT NULL -- Timestamp de criação
);
```
@@ -81,7 +89,10 @@ SELECT COUNT(*) FROM pastes;
SELECT * FROM pastes WHERE date(created_at, 'unixepoch') = date('now');
-- Pastes maiores que 1KB
SELECT hash, length(content) as size FROM pastes WHERE length(content) > 1024;
SELECT short_id, length(content) as size FROM pastes WHERE length(content) > 1024;
-- Encontrar duplicatas (mesmo conteúdo, IDs diferentes)
SELECT content_hash, COUNT(*) as count FROM pastes GROUP BY content_hash HAVING count > 1;
```
## Copyright

114
SHORT_IDS_SUMMARY.md Normal file
View File

@@ -0,0 +1,114 @@
# Resumo: Implementação de IDs Curtos
## ✅ Implementação Concluída com Sucesso!
O sistema YASUC foi atualizado para usar IDs curtos de 7 caracteres em vez de hashes SHA-256 completos.
## 🔄 Mudanças Principais
### 1. **Sistema de IDs Curtos**
-**7 caracteres**: Combinação de letras maiúsculas, minúsculas e números
-**62^7 possibilidades**: Aproximadamente 3.5 trilhões de combinações únicas
-**Geração aleatória**: IDs são gerados aleatoriamente
-**Detecção de duplicatas**: Pastes idênticos recebem o mesmo ID
### 2. **Estrutura do Banco Atualizada**
```sql
CREATE TABLE pastes (
short_id TEXT PRIMARY KEY, -- ID curto único (7 caracteres)
content TEXT NOT NULL, -- Conteúdo do paste
content_hash TEXT NOT NULL, -- Hash SHA-256 para detecção de duplicatas
created_at INTEGER NOT NULL -- Timestamp de criação
);
```
### 3. **Índices de Performance**
-`idx_pastes_content_hash` - Para detecção rápida de duplicatas
-`idx_pastes_created_at` - Para consultas por data
### 4. **Funcionalidades Implementadas**
- ✅ Geração de IDs únicos com verificação de colisões
- ✅ Detecção de duplicatas baseada no hash SHA-256 do conteúdo
- ✅ Retorno do mesmo ID para pastes idênticos
- ✅ Tratamento de erros para colisões de IDs
## 🧪 Testes Realizados
### ✅ Funcionalidade Básica
- ✅ Geração de IDs curtos: `LV86NjG`, `dMZWovL`
- ✅ URLs mais amigáveis: `http://localhost:8080/LV86NjG`
- ✅ Recuperação de pastes funciona corretamente
- ✅ Detecção de duplicatas funciona
### ✅ Testes de Duplicatas
- ✅ Paste idêntico retorna o mesmo ID
- ✅ Paste diferente gera novo ID
- ✅ Sistema mantém integridade dos dados
## 📊 Comparação: Antes vs Agora
| Aspecto | Antes | Agora |
|---------|-------|-------|
| **URL** | `http://localhost:8080/a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447` | `http://localhost:8080/Ly34kx3` |
| **Tamanho** | 64 caracteres | 7 caracteres |
| **Legibilidade** | Difícil de ler/digitar | Fácil de ler/digitar |
| **Compartilhamento** | URLs longas e complexas | URLs curtas e amigáveis |
| **Duplicatas** | Mesmo hash = mesma URL | Mesmo conteúdo = mesmo ID |
## 🔧 Vantagens dos IDs Curtos
1. **Usabilidade**: URLs mais fáceis de compartilhar e digitar
2. **Legibilidade**: IDs curtos são mais humanos
3. **Compatibilidade**: Funciona melhor em sistemas com limitações de URL
4. **UX**: Melhor experiência do usuário
5. **Manutenção**: Mais fácil de gerenciar e debugar
## 📁 Arquivos Modificados
### Código Principal:
- `yasuc.go` - Sistema de IDs curtos implementado
- `cmd/migrate/main.go` - Script de migração atualizado
### Documentação:
- `README.md` - Documentação atualizada
- `MIGRATION.md` - Instruções de migração atualizadas
- `init.sql` - Script SQL atualizado
## 🚀 Como Usar
### Execução Local:
```bash
go build -o yasuc.exe .
./yasuc.exe -db data/yasuc.sqlite3 -port 8080 -addr 127.0.0.1
```
### Teste:
```bash
# Criar paste
echo "hello world" | curl -F "sprunge=<-" http://localhost:8080/
# Retorna: http://localhost:8080/Ly34kx3
# Acessar paste
curl http://localhost:8080/Ly34kx3
# Retorna: hello world
```
## ⚠️ Notas Importantes
1. **URLs antigas não funcionam**: Pastes migrados terão novos IDs
2. **Migração necessária**: Dados existentes precisam ser migrados
3. **Detecção de duplicatas**: Funciona baseada no conteúdo, não no ID
4. **Colisões**: Extremamente raras, mas tratadas automaticamente
## 🎯 Próximos Passos (Opcionais)
1. **Migrar dados existentes**: Use o script de migração
2. **Personalizar charset**: Modificar caracteres usados nos IDs
3. **Ajustar tamanho**: Alterar comprimento dos IDs se necessário
4. **Adicionar prefixos**: Para diferentes tipos de paste
## ✅ Status Final
**IMPLEMENTAÇÃO CONCLUÍDA COM SUCESSO!**
O sistema agora gera URLs curtas e amigáveis mantendo toda a funcionalidade anterior, incluindo detecção de duplicatas e integridade dos dados.

View File

@@ -7,6 +7,7 @@ import (
"flag"
"fmt"
"log"
"math/rand"
"os"
"time"
@@ -16,7 +17,41 @@ import (
_ "modernc.org/sqlite"
)
const shortIDLength = 7
// generateShortID gera um ID único de 7 caracteres
func generateShortID() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, shortIDLength)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// generateUniqueShortID gera um ID único que não existe no banco
func generateUniqueShortID(db *sql.DB) (string, error) {
maxAttempts := 100
for i := 0; i < maxAttempts; i++ {
id := generateShortID()
// Verificar se o ID já existe
var exists int
err := db.QueryRow("SELECT 1 FROM pastes WHERE short_id = ? LIMIT 1", id).Scan(&exists)
if err == sql.ErrNoRows {
return id, nil
}
if err != nil {
return "", err
}
}
return "", fmt.Errorf("não foi possível gerar um ID único após %d tentativas", maxAttempts)
}
func main() {
// Initialize random seed
rand.Seed(time.Now().UnixNano())
var boltPath, sqlitePath string
flag.StringVar(&boltPath, "bolt", "", "caminho para o arquivo BoltDB")
flag.StringVar(&sqlitePath, "sqlite", "", "caminho para o arquivo SQLite3 de destino")
@@ -44,8 +79,9 @@ func main() {
// Criar tabela no SQLite3
_, err = sqliteDB.Exec(`
CREATE TABLE IF NOT EXISTS pastes (
hash TEXT PRIMARY KEY,
short_id TEXT PRIMARY KEY,
content TEXT NOT NULL,
content_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`)
@@ -53,6 +89,17 @@ func main() {
log.Fatal("Erro ao criar tabela:", err)
}
// Criar índices
_, err = sqliteDB.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_content_hash ON pastes(content_hash)`)
if err != nil {
log.Fatal("Erro ao criar índice content_hash:", err)
}
_, err = sqliteDB.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_created_at ON pastes(created_at)`)
if err != nil {
log.Fatal("Erro ao criar índice created_at:", err)
}
// Iniciar transação no SQLite3
tx, err := sqliteDB.Begin()
if err != nil {
@@ -60,7 +107,7 @@ func main() {
}
// Preparar statement de inserção
stmt, err := tx.Prepare("INSERT OR IGNORE INTO pastes (hash, content, created_at) VALUES (?, ?, ?)")
stmt, err := tx.Prepare("INSERT OR IGNORE INTO pastes (short_id, content, content_hash, created_at) VALUES (?, ?, ?, ?)")
if err != nil {
log.Fatal("Erro ao preparar statement:", err)
}
@@ -68,6 +115,7 @@ func main() {
// Migrar dados do BoltDB para SQLite3
count := 0
duplicates := 0
err = boltDB.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("pastes"))
if b == nil {
@@ -79,10 +127,27 @@ func main() {
hash := sha256.Sum256(v)
hashStr := hex.EncodeToString(hash[:])
// Inserir no SQLite3
_, err := stmt.Exec(hashStr, string(v), time.Now().Unix())
// Verificar se já existe um paste com o mesmo conteúdo
var existingID string
err := sqliteDB.QueryRow("SELECT short_id FROM pastes WHERE content_hash = ? LIMIT 1", hashStr).Scan(&existingID)
if err == nil {
// Paste já existe, pular
duplicates++
return nil
} else if err != sql.ErrNoRows {
return fmt.Errorf("erro ao verificar duplicata: %v", err)
}
// Gerar ID curto único
shortID, err := generateUniqueShortID(sqliteDB)
if err != nil {
return fmt.Errorf("erro ao inserir paste %s: %v", hashStr, err)
return fmt.Errorf("erro ao gerar ID único: %v", err)
}
// Inserir no SQLite3
_, err = stmt.Exec(shortID, string(v), hashStr, time.Now().Unix())
if err != nil {
return fmt.Errorf("erro ao inserir paste %s: %v", shortID, err)
}
count++
@@ -105,5 +170,5 @@ func main() {
log.Fatal("Erro ao fazer commit:", err)
}
fmt.Printf("Migração concluída! %d pastes migrados com sucesso.\n", count)
fmt.Printf("Migração concluída! %d pastes migrados com sucesso, %d duplicatas ignoradas.\n", count, duplicates)
}

Binary file not shown.

View File

@@ -2,14 +2,16 @@
-- Execute este script para criar a estrutura do banco
CREATE TABLE IF NOT EXISTS pastes (
hash TEXT PRIMARY KEY,
short_id TEXT PRIMARY KEY,
content TEXT NOT NULL,
content_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
);
-- Criar índice para melhorar performance de consultas por data
-- Criar índices para melhorar performance
CREATE INDEX IF NOT EXISTS idx_pastes_content_hash ON pastes(content_hash);
CREATE INDEX IF NOT EXISTS idx_pastes_created_at ON pastes(created_at);
-- Inserir alguns dados de exemplo (opcional)
-- INSERT OR IGNORE INTO pastes (hash, content, created_at) VALUES
-- ('a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447', 'hello world', strftime('%s', 'now'));
-- INSERT OR IGNORE INTO pastes (short_id, content, content_hash, created_at) VALUES
-- ('Ly34kx3', 'hello world', 'a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447', strftime('%s', 'now'));

BIN
yasuc.exe

Binary file not shown.

View File

@@ -25,6 +25,7 @@ import (
"fmt"
"html/template"
"log"
"math/rand"
"net/http"
"os"
"time"
@@ -33,7 +34,8 @@ import (
)
const (
maxPasteSize = 4 * 1024 * 1024
maxPasteSize = 4 * 1024 * 1024
shortIDLength = 7
)
const usageText = `
@@ -53,13 +55,12 @@ SYNOPSIS
DESCRIPTION
A command line pastebin. Pastes are immutable and created with simple HTTP
POST requests. The path of a paste's URL is the SHA-256 digest of the
paste's contents.
POST requests. The path of a paste's URL is a short unique identifier.
EXAMPLE
$ echo 'hello world' | curl -F 'sprunge=&lt;-' {{.BaseURL}}
{{.BaseURL}}/a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
$ firefox {{.BaseURL}}/a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
{{.BaseURL}}/Ly34kx3
$ firefox {{.BaseURL}}/Ly34kx3
COPYRIGHT
Copyright © Tom Jakubowski. License AGPLv3: GNU Affero GPL Version 3
@@ -82,23 +83,70 @@ func (e pasteTooLarge) Error() string {
return fmt.Sprintf("paste too large (maximum size %d bytes)", maxPasteSize)
}
// generateShortID gera um ID único de 7 caracteres
func generateShortID() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, shortIDLength)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// generateUniqueShortID gera um ID único que não existe no banco
func generateUniqueShortID(db *sql.DB) (string, error) {
maxAttempts := 100
for i := 0; i < maxAttempts; i++ {
id := generateShortID()
// Verificar se o ID já existe
var exists int
err := db.QueryRow("SELECT 1 FROM pastes WHERE short_id = ? LIMIT 1", id).Scan(&exists)
if err == sql.ErrNoRows {
return id, nil
}
if err != nil {
return "", err
}
}
return "", fmt.Errorf("não foi possível gerar um ID único após %d tentativas", maxAttempts)
}
func stashPaste(db *sql.DB, pasteStr string) (key string, err error) {
if len(pasteStr) > maxPasteSize {
err = pasteTooLarge{}
return
}
paste := []byte(pasteStr)
rawKey := sha256.Sum256(paste)
key = hex.EncodeToString(rawKey[:])
// Insert or ignore (SQLite's UPSERT equivalent)
_, err = db.Exec("INSERT OR IGNORE INTO pastes (hash, content, created_at) VALUES (?, ?, ?)",
key, pasteStr, time.Now().Unix())
// Gerar ID curto único
shortID, err := generateUniqueShortID(db)
if err != nil {
return "", err
}
return key, nil
// Calcular hash SHA-256 para verificação de duplicatas
paste := []byte(pasteStr)
hash := sha256.Sum256(paste)
hashStr := hex.EncodeToString(hash[:])
// Verificar se já existe um paste com o mesmo conteúdo
var existingID string
err = db.QueryRow("SELECT short_id FROM pastes WHERE content_hash = ? LIMIT 1", hashStr).Scan(&existingID)
if err == nil {
// Paste já existe, retornar o ID existente
return existingID, nil
} else if err != sql.ErrNoRows {
return "", err
}
// Inserir novo paste
_, err = db.Exec("INSERT INTO pastes (short_id, content, content_hash, created_at) VALUES (?, ?, ?, ?)",
shortID, pasteStr, hashStr, time.Now().Unix())
if err != nil {
return "", err
}
return shortID, nil
}
type pasteNotFound struct{}
@@ -109,7 +157,7 @@ func (e pasteNotFound) Error() string {
func fetchPaste(db *sql.DB, key string) (paste string, err error) {
var content string
err = db.QueryRow("SELECT content FROM pastes WHERE hash = ?", key).Scan(&content)
err = db.QueryRow("SELECT content FROM pastes WHERE short_id = ?", key).Scan(&content)
if err != nil {
if err == sql.ErrNoRows {
return "", pasteNotFound{}
@@ -177,15 +225,30 @@ func initDatabase(db *sql.DB) error {
// Create the pastes table if it doesn't exist
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS pastes (
hash TEXT PRIMARY KEY,
short_id TEXT PRIMARY KEY,
content TEXT NOT NULL,
content_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`)
if err != nil {
return err
}
// Create indexes for better performance
_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_content_hash ON pastes(content_hash)`)
if err != nil {
return err
}
_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_pastes_created_at ON pastes(created_at)`)
return err
}
func main() {
// Initialize random seed
rand.Seed(time.Now().UnixNano())
var dbPath, addr string
var port int
flag.StringVar(&dbPath, "db", "", "location of database file")