mega update storage

This commit is contained in:
Marco Pedone 2026-05-19 18:42:34 +02:00
parent c517dbcc7a
commit 48b31a5491
77 changed files with 2037 additions and 1716 deletions

View file

@ -14,15 +14,6 @@ import (
// Config holds all application configuration
type Config struct {
Database struct {
Host string
Port int
User string
Password string
DBName string
SSLMode string
}
Images struct {
Enabled bool
ConcurrentLimit int
@ -61,14 +52,6 @@ func Load() *Config {
os.Exit(1)
}
// Database config
Cfg.Database.Host = getEnv("PGHOST", "localhost")
Cfg.Database.Port = getEnvAsInt("PGPORT", 5432)
Cfg.Database.User = getEnv("POSTGRES_USER", "postgres")
Cfg.Database.Password = getEnv("POSTGRES_PASSWORD", "")
Cfg.Database.DBName = getEnv("POSTGRES_DB", "postgres")
Cfg.Database.SSLMode = getEnv("PGSSLMODE", "true")
// Image processing config
Cfg.Images.Enabled = getEnvAsBool("IMAGEOPTION", true)
Cfg.Images.ConcurrentLimit = getEnvAsInt("CONCURRENT_IMAGES", 5)
@ -86,10 +69,6 @@ func Load() *Config {
// Server config
Cfg.Server.Port = getEnvAsInt("PORT", 1323)
// Validate required config
if Cfg.Database.Password == "" {
fmt.Printf("required environment variable POSTGRES_PASSWORD not set\n")
}
})
if err := exec.Command("ffmpeg", "-version").Run(); err != nil {

View file

@ -1,59 +0,0 @@
package db
import (
"context"
"fmt"
"log/slog"
pgxuuid "github.com/jackc/pgx-gofrs-uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type DbHandler struct {
Dbpool *pgxpool.Pool
Ctx context.Context
}
type Configs struct {
Host string
Dbname string
Port string
User string
Password string
LoggerEnabled bool
}
func NewDbHandler(configs Configs) *DbHandler {
fmt.Println("Connecting to db")
ctx := context.Background()
if configs.Host == "" || configs.Dbname == "" || configs.Port == "" || configs.User == "" || configs.Password == "" {
fmt.Println("Environment variables for database connection are not set")
return nil
}
connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", configs.User, configs.Password, configs.Host, configs.Port, configs.Dbname)
fmt.Printf("Db connection string: %s\n", connStr)
config, err := pgxpool.ParseConfig(connStr)
if err != nil {
panic(err)
}
if configs.LoggerEnabled {
fmt.Println("Database query logging is enabled")
config.ConnConfig.Tracer = NewMultiQueryTracer(NewLoggingQueryTracer(slog.Default()))
}
config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
pgxuuid.Register(conn.TypeMap())
return nil
}
dbpool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
panic(err)
}
fmt.Println("Connected to db")
return &DbHandler{Dbpool: dbpool, Ctx: ctx}
}

View file

@ -1,136 +0,0 @@
package db
import (
"context"
"fmt"
"log/slog"
"regexp"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// obfuscate replaces the first n characters of a string with asterisks.
func obfuscate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + strings.Repeat("*", len(s)-n)
}
// ConnectToDatabaseWithLogging connects to the database with logging. It tests
// the connection and returns the connection pool.
func ConnectToDatabaseWithLogging(ctx context.Context, connectionString string, loggingEnabled bool) (*pgxpool.Pool, error) {
// Parse the connection string
slog.
Info("parsing connection string",
slog.String("connection_string", obfuscate(connectionString, 10)),
)
config, err := pgxpool.ParseConfig(connectionString)
if err != nil {
return nil, fmt.Errorf("failed to parse connection string: %w", err)
}
// If logging is enabled, set the tracer
if loggingEnabled {
config.ConnConfig.Tracer = NewMultiQueryTracer(NewLoggingQueryTracer(slog.Default()))
}
// Create the connection pool
slog.Info("creating connection pool")
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %w", err)
}
// Ping the database
slog.Info("pinging database")
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return pool, nil
}
var (
replaceTabs = regexp.MustCompile(`\t+`)
replaceSpacesBeforeOpeningParens = regexp.MustCompile(`\s+\(`)
replaceSpacesAfterOpeningParens = regexp.MustCompile(`\(\s+`)
replaceSpacesBeforeClosingParens = regexp.MustCompile(`\s+\)`)
replaceSpacesAfterClosingParens = regexp.MustCompile(`\)\s+`)
replaceSpaces = regexp.MustCompile(`\s+`)
)
// prettyPrintSQL removes empty lines and trims spaces.
func prettyPrintSQL(sql string) string {
lines := strings.Split(sql, "\n")
pretty := strings.Join(lines, " ")
pretty = replaceTabs.ReplaceAllString(pretty, "")
pretty = replaceSpacesBeforeOpeningParens.ReplaceAllString(pretty, "(")
pretty = replaceSpacesAfterOpeningParens.ReplaceAllString(pretty, "(")
pretty = replaceSpacesAfterClosingParens.ReplaceAllString(pretty, ")")
pretty = replaceSpacesBeforeClosingParens.ReplaceAllString(pretty, ")")
// Finally, replace multiple spaces with a single space
pretty = replaceSpaces.ReplaceAllString(pretty, " ")
return strings.TrimSpace(pretty)
}
// https://github.com/jackc/pgx/issues/1061#issuecomment-1186250809
type LoggingQueryTracer struct {
logger *slog.Logger
}
func NewLoggingQueryTracer(logger *slog.Logger) *LoggingQueryTracer {
return &LoggingQueryTracer{logger: logger}
}
func (l *LoggingQueryTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
l.logger.
Info("query start",
slog.String("sql", prettyPrintSQL(data.SQL)),
slog.Any("args", data.Args),
)
return ctx
}
func (l *LoggingQueryTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
// Failure
if data.Err != nil {
l.logger.
Error("query end",
slog.String("error", data.Err.Error()),
slog.String("command_tag", data.CommandTag.String()),
)
return
}
// Success
l.logger.
Info("query end",
slog.String("command_tag", data.CommandTag.String()),
)
}
// https://github.com/jackc/pgx/discussions/1677#discussioncomment-8815982
type MultiQueryTracer struct {
Tracers []pgx.QueryTracer
}
func NewMultiQueryTracer(tracers ...pgx.QueryTracer) *MultiQueryTracer {
return &MultiQueryTracer{Tracers: tracers}
}
func (m *MultiQueryTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
for _, t := range m.Tracers {
ctx = t.TraceQueryStart(ctx, conn, data)
}
return ctx
}
func (m *MultiQueryTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
for _, t := range m.Tracers {
t.TraceQueryEnd(ctx, conn, data)
}
}

View file

@ -0,0 +1,68 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/lib/pq"
"time"
)
type Annunci struct {
ID int32 `sql:"primary_key"`
Codice string
Locatore *string
Numero *string
Idlocatore *string
Indirizzo *string
Civico *string
Comune *string
Cap *string
Provincia *string
Regione *string
Lat *string
Lon *string
IndirizzoSecondario *string
CivicoSecondario *string
LatSecondario *string
LonSecondario *string
Tipo *string
Categorie *pq.StringArray
Prezzo int32
Anno *string
Classe *string
Mq *float64
Piano *string
PianoPalazzo *int32
UnitaCondominio *int32
NumeroVani *int32
NumeroCamere *int32
NumeroBagni *int32
NumeroBalconi *int32
NumeroTerrazzi *int32
NumeroBox *int32
NumeroPostiauto *int32
Accessori *pq.StringArray
TitoloIt *string
DescIt *string
TitoloEn *string
DescEn *string
Stato *string
Web *bool
Caratteristiche *string // @type(Caratteristiche, 'src/utils/kanel-types.ts', false, false, true)
Homepage *bool
ExternalVideos *pq.StringArray
Email *string
CreatoIl *time.Time
ModificatoIl *time.Time
Consegna *int32
DisponibileDa *time.Time
Permanenza *pq.Int32Array
Persone *pq.StringArray
MediaUpdatedAt *time.Time
TipoLocatore *string
}

View file

@ -0,0 +1,20 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type ImagesRefs struct {
Codice string
Ordine int32
Img uuid.UUID
Thumb uuid.UUID
OgURL string
}

View file

@ -0,0 +1,24 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type Storage struct {
ID uuid.UUID `sql:"primary_key"`
CreatedAt time.Time
Filename string
MimeType string
FileSizeBytes int32
FileData []byte
ExpiresAt *time.Time
Tag *string
}

View file

@ -0,0 +1,20 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type VideosRefs struct {
Codice string
Ordine int32
Video uuid.UUID
Thumb uuid.UUID
OgURL string
}

View file

@ -0,0 +1,231 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Annunci = newAnnunciTable("public", "annunci", "")
type annunciTable struct {
postgres.Table
// Columns
ID postgres.ColumnInteger
Codice postgres.ColumnString
Locatore postgres.ColumnString
Numero postgres.ColumnString
Idlocatore postgres.ColumnString
Indirizzo postgres.ColumnString
Civico postgres.ColumnString
Comune postgres.ColumnString
Cap postgres.ColumnString
Provincia postgres.ColumnString
Regione postgres.ColumnString
Lat postgres.ColumnString
Lon postgres.ColumnString
IndirizzoSecondario postgres.ColumnString
CivicoSecondario postgres.ColumnString
LatSecondario postgres.ColumnString
LonSecondario postgres.ColumnString
Tipo postgres.ColumnString
Categorie postgres.ColumnStringArray
Prezzo postgres.ColumnInteger
Anno postgres.ColumnString
Classe postgres.ColumnString
Mq postgres.ColumnFloat
Piano postgres.ColumnString
PianoPalazzo postgres.ColumnInteger
UnitaCondominio postgres.ColumnInteger
NumeroVani postgres.ColumnInteger
NumeroCamere postgres.ColumnInteger
NumeroBagni postgres.ColumnInteger
NumeroBalconi postgres.ColumnInteger
NumeroTerrazzi postgres.ColumnInteger
NumeroBox postgres.ColumnInteger
NumeroPostiauto postgres.ColumnInteger
Accessori postgres.ColumnStringArray
TitoloIt postgres.ColumnString
DescIt postgres.ColumnString
TitoloEn postgres.ColumnString
DescEn postgres.ColumnString
Stato postgres.ColumnString
Web postgres.ColumnBool
Caratteristiche postgres.ColumnString // @type(Caratteristiche, 'src/utils/kanel-types.ts', false, false, true)
Homepage postgres.ColumnBool
ExternalVideos postgres.ColumnStringArray
Email postgres.ColumnString
CreatoIl postgres.ColumnTimestamp
ModificatoIl postgres.ColumnTimestamp
Consegna postgres.ColumnInteger
DisponibileDa postgres.ColumnDate
Permanenza postgres.ColumnIntegerArray
Persone postgres.ColumnStringArray
MediaUpdatedAt postgres.ColumnTimestamp
TipoLocatore postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type AnnunciTable struct {
annunciTable
EXCLUDED annunciTable
}
// AS creates new AnnunciTable with assigned alias
func (a AnnunciTable) AS(alias string) *AnnunciTable {
return newAnnunciTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new AnnunciTable with assigned schema name
func (a AnnunciTable) FromSchema(schemaName string) *AnnunciTable {
return newAnnunciTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new AnnunciTable with assigned table prefix
func (a AnnunciTable) WithPrefix(prefix string) *AnnunciTable {
return newAnnunciTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new AnnunciTable with assigned table suffix
func (a AnnunciTable) WithSuffix(suffix string) *AnnunciTable {
return newAnnunciTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newAnnunciTable(schemaName, tableName, alias string) *AnnunciTable {
return &AnnunciTable{
annunciTable: newAnnunciTableImpl(schemaName, tableName, alias),
EXCLUDED: newAnnunciTableImpl("", "excluded", ""),
}
}
func newAnnunciTableImpl(schemaName, tableName, alias string) annunciTable {
var (
IDColumn = postgres.IntegerColumn("id")
CodiceColumn = postgres.StringColumn("codice")
LocatoreColumn = postgres.StringColumn("locatore")
NumeroColumn = postgres.StringColumn("numero")
IdlocatoreColumn = postgres.StringColumn("idlocatore")
IndirizzoColumn = postgres.StringColumn("indirizzo")
CivicoColumn = postgres.StringColumn("civico")
ComuneColumn = postgres.StringColumn("comune")
CapColumn = postgres.StringColumn("cap")
ProvinciaColumn = postgres.StringColumn("provincia")
RegioneColumn = postgres.StringColumn("regione")
LatColumn = postgres.StringColumn("lat")
LonColumn = postgres.StringColumn("lon")
IndirizzoSecondarioColumn = postgres.StringColumn("indirizzo_secondario")
CivicoSecondarioColumn = postgres.StringColumn("civico_secondario")
LatSecondarioColumn = postgres.StringColumn("lat_secondario")
LonSecondarioColumn = postgres.StringColumn("lon_secondario")
TipoColumn = postgres.StringColumn("tipo")
CategorieColumn = postgres.StringArrayColumn("categorie")
PrezzoColumn = postgres.IntegerColumn("prezzo")
AnnoColumn = postgres.StringColumn("anno")
ClasseColumn = postgres.StringColumn("classe")
MqColumn = postgres.FloatColumn("mq")
PianoColumn = postgres.StringColumn("piano")
PianoPalazzoColumn = postgres.IntegerColumn("piano_palazzo")
UnitaCondominioColumn = postgres.IntegerColumn("unita_condominio")
NumeroVaniColumn = postgres.IntegerColumn("numero_vani")
NumeroCamereColumn = postgres.IntegerColumn("numero_camere")
NumeroBagniColumn = postgres.IntegerColumn("numero_bagni")
NumeroBalconiColumn = postgres.IntegerColumn("numero_balconi")
NumeroTerrazziColumn = postgres.IntegerColumn("numero_terrazzi")
NumeroBoxColumn = postgres.IntegerColumn("numero_box")
NumeroPostiautoColumn = postgres.IntegerColumn("numero_postiauto")
AccessoriColumn = postgres.StringArrayColumn("accessori")
TitoloItColumn = postgres.StringColumn("titolo_it")
DescItColumn = postgres.StringColumn("desc_it")
TitoloEnColumn = postgres.StringColumn("titolo_en")
DescEnColumn = postgres.StringColumn("desc_en")
StatoColumn = postgres.StringColumn("stato")
WebColumn = postgres.BoolColumn("web")
CaratteristicheColumn = postgres.StringColumn("caratteristiche")
HomepageColumn = postgres.BoolColumn("homepage")
ExternalVideosColumn = postgres.StringArrayColumn("external_videos")
EmailColumn = postgres.StringColumn("email")
CreatoIlColumn = postgres.TimestampColumn("creato_il")
ModificatoIlColumn = postgres.TimestampColumn("modificato_il")
ConsegnaColumn = postgres.IntegerColumn("consegna")
DisponibileDaColumn = postgres.DateColumn("disponibile_da")
PermanenzaColumn = postgres.IntegerArrayColumn("permanenza")
PersoneColumn = postgres.StringArrayColumn("persone")
MediaUpdatedAtColumn = postgres.TimestampColumn("media_updated_at")
TipoLocatoreColumn = postgres.StringColumn("tipo_locatore")
allColumns = postgres.ColumnList{IDColumn, CodiceColumn, LocatoreColumn, NumeroColumn, IdlocatoreColumn, IndirizzoColumn, CivicoColumn, ComuneColumn, CapColumn, ProvinciaColumn, RegioneColumn, LatColumn, LonColumn, IndirizzoSecondarioColumn, CivicoSecondarioColumn, LatSecondarioColumn, LonSecondarioColumn, TipoColumn, CategorieColumn, PrezzoColumn, AnnoColumn, ClasseColumn, MqColumn, PianoColumn, PianoPalazzoColumn, UnitaCondominioColumn, NumeroVaniColumn, NumeroCamereColumn, NumeroBagniColumn, NumeroBalconiColumn, NumeroTerrazziColumn, NumeroBoxColumn, NumeroPostiautoColumn, AccessoriColumn, TitoloItColumn, DescItColumn, TitoloEnColumn, DescEnColumn, StatoColumn, WebColumn, CaratteristicheColumn, HomepageColumn, ExternalVideosColumn, EmailColumn, CreatoIlColumn, ModificatoIlColumn, ConsegnaColumn, DisponibileDaColumn, PermanenzaColumn, PersoneColumn, MediaUpdatedAtColumn, TipoLocatoreColumn}
mutableColumns = postgres.ColumnList{CodiceColumn, LocatoreColumn, NumeroColumn, IdlocatoreColumn, IndirizzoColumn, CivicoColumn, ComuneColumn, CapColumn, ProvinciaColumn, RegioneColumn, LatColumn, LonColumn, IndirizzoSecondarioColumn, CivicoSecondarioColumn, LatSecondarioColumn, LonSecondarioColumn, TipoColumn, CategorieColumn, PrezzoColumn, AnnoColumn, ClasseColumn, MqColumn, PianoColumn, PianoPalazzoColumn, UnitaCondominioColumn, NumeroVaniColumn, NumeroCamereColumn, NumeroBagniColumn, NumeroBalconiColumn, NumeroTerrazziColumn, NumeroBoxColumn, NumeroPostiautoColumn, AccessoriColumn, TitoloItColumn, DescItColumn, TitoloEnColumn, DescEnColumn, StatoColumn, WebColumn, CaratteristicheColumn, HomepageColumn, ExternalVideosColumn, EmailColumn, CreatoIlColumn, ModificatoIlColumn, ConsegnaColumn, DisponibileDaColumn, PermanenzaColumn, PersoneColumn, MediaUpdatedAtColumn, TipoLocatoreColumn}
defaultColumns = postgres.ColumnList{IDColumn, PrezzoColumn}
)
return annunciTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Codice: CodiceColumn,
Locatore: LocatoreColumn,
Numero: NumeroColumn,
Idlocatore: IdlocatoreColumn,
Indirizzo: IndirizzoColumn,
Civico: CivicoColumn,
Comune: ComuneColumn,
Cap: CapColumn,
Provincia: ProvinciaColumn,
Regione: RegioneColumn,
Lat: LatColumn,
Lon: LonColumn,
IndirizzoSecondario: IndirizzoSecondarioColumn,
CivicoSecondario: CivicoSecondarioColumn,
LatSecondario: LatSecondarioColumn,
LonSecondario: LonSecondarioColumn,
Tipo: TipoColumn,
Categorie: CategorieColumn,
Prezzo: PrezzoColumn,
Anno: AnnoColumn,
Classe: ClasseColumn,
Mq: MqColumn,
Piano: PianoColumn,
PianoPalazzo: PianoPalazzoColumn,
UnitaCondominio: UnitaCondominioColumn,
NumeroVani: NumeroVaniColumn,
NumeroCamere: NumeroCamereColumn,
NumeroBagni: NumeroBagniColumn,
NumeroBalconi: NumeroBalconiColumn,
NumeroTerrazzi: NumeroTerrazziColumn,
NumeroBox: NumeroBoxColumn,
NumeroPostiauto: NumeroPostiautoColumn,
Accessori: AccessoriColumn,
TitoloIt: TitoloItColumn,
DescIt: DescItColumn,
TitoloEn: TitoloEnColumn,
DescEn: DescEnColumn,
Stato: StatoColumn,
Web: WebColumn,
Caratteristiche: CaratteristicheColumn,
Homepage: HomepageColumn,
ExternalVideos: ExternalVideosColumn,
Email: EmailColumn,
CreatoIl: CreatoIlColumn,
ModificatoIl: ModificatoIlColumn,
Consegna: ConsegnaColumn,
DisponibileDa: DisponibileDaColumn,
Permanenza: PermanenzaColumn,
Persone: PersoneColumn,
MediaUpdatedAt: MediaUpdatedAtColumn,
TipoLocatore: TipoLocatoreColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View file

@ -0,0 +1,90 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ImagesRefs = newImagesRefsTable("public", "images_refs", "")
type imagesRefsTable struct {
postgres.Table
// Columns
Codice postgres.ColumnString
Ordine postgres.ColumnInteger
Img postgres.ColumnString
Thumb postgres.ColumnString
OgURL postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ImagesRefsTable struct {
imagesRefsTable
EXCLUDED imagesRefsTable
}
// AS creates new ImagesRefsTable with assigned alias
func (a ImagesRefsTable) AS(alias string) *ImagesRefsTable {
return newImagesRefsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImagesRefsTable with assigned schema name
func (a ImagesRefsTable) FromSchema(schemaName string) *ImagesRefsTable {
return newImagesRefsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImagesRefsTable with assigned table prefix
func (a ImagesRefsTable) WithPrefix(prefix string) *ImagesRefsTable {
return newImagesRefsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImagesRefsTable with assigned table suffix
func (a ImagesRefsTable) WithSuffix(suffix string) *ImagesRefsTable {
return newImagesRefsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImagesRefsTable(schemaName, tableName, alias string) *ImagesRefsTable {
return &ImagesRefsTable{
imagesRefsTable: newImagesRefsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImagesRefsTableImpl("", "excluded", ""),
}
}
func newImagesRefsTableImpl(schemaName, tableName, alias string) imagesRefsTable {
var (
CodiceColumn = postgres.StringColumn("codice")
OrdineColumn = postgres.IntegerColumn("ordine")
ImgColumn = postgres.StringColumn("img")
ThumbColumn = postgres.StringColumn("thumb")
OgURLColumn = postgres.StringColumn("og_url")
allColumns = postgres.ColumnList{CodiceColumn, OrdineColumn, ImgColumn, ThumbColumn, OgURLColumn}
mutableColumns = postgres.ColumnList{CodiceColumn, OrdineColumn, ImgColumn, ThumbColumn, OgURLColumn}
defaultColumns = postgres.ColumnList{}
)
return imagesRefsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
Codice: CodiceColumn,
Ordine: OrdineColumn,
Img: ImgColumn,
Thumb: ThumbColumn,
OgURL: OgURLColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View file

@ -0,0 +1,99 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Storage = newStorageTable("public", "storage", "")
type storageTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
CreatedAt postgres.ColumnTimestamp
Filename postgres.ColumnString
MimeType postgres.ColumnString
FileSizeBytes postgres.ColumnInteger
FileData postgres.ColumnBytea
ExpiresAt postgres.ColumnTimestamp
Tag postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type StorageTable struct {
storageTable
EXCLUDED storageTable
}
// AS creates new StorageTable with assigned alias
func (a StorageTable) AS(alias string) *StorageTable {
return newStorageTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new StorageTable with assigned schema name
func (a StorageTable) FromSchema(schemaName string) *StorageTable {
return newStorageTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new StorageTable with assigned table prefix
func (a StorageTable) WithPrefix(prefix string) *StorageTable {
return newStorageTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new StorageTable with assigned table suffix
func (a StorageTable) WithSuffix(suffix string) *StorageTable {
return newStorageTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newStorageTable(schemaName, tableName, alias string) *StorageTable {
return &StorageTable{
storageTable: newStorageTableImpl(schemaName, tableName, alias),
EXCLUDED: newStorageTableImpl("", "excluded", ""),
}
}
func newStorageTableImpl(schemaName, tableName, alias string) storageTable {
var (
IDColumn = postgres.StringColumn("id")
CreatedAtColumn = postgres.TimestampColumn("created_at")
FilenameColumn = postgres.StringColumn("filename")
MimeTypeColumn = postgres.StringColumn("mime_type")
FileSizeBytesColumn = postgres.IntegerColumn("file_size_bytes")
FileDataColumn = postgres.ByteaColumn("file_data")
ExpiresAtColumn = postgres.TimestampColumn("expires_at")
TagColumn = postgres.StringColumn("tag")
allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, FilenameColumn, MimeTypeColumn, FileSizeBytesColumn, FileDataColumn, ExpiresAtColumn, TagColumn}
mutableColumns = postgres.ColumnList{CreatedAtColumn, FilenameColumn, MimeTypeColumn, FileSizeBytesColumn, FileDataColumn, ExpiresAtColumn, TagColumn}
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
)
return storageTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
CreatedAt: CreatedAtColumn,
Filename: FilenameColumn,
MimeType: MimeTypeColumn,
FileSizeBytes: FileSizeBytesColumn,
FileData: FileDataColumn,
ExpiresAt: ExpiresAtColumn,
Tag: TagColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View file

@ -0,0 +1,17 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
// this method only once at the beginning of the program.
func UseSchema(schema string) {
Annunci = Annunci.FromSchema(schema)
ImagesRefs = ImagesRefs.FromSchema(schema)
Storage = Storage.FromSchema(schema)
VideosRefs = VideosRefs.FromSchema(schema)
}

View file

@ -0,0 +1,90 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var VideosRefs = newVideosRefsTable("public", "videos_refs", "")
type videosRefsTable struct {
postgres.Table
// Columns
Codice postgres.ColumnString
Ordine postgres.ColumnInteger
Video postgres.ColumnString
Thumb postgres.ColumnString
OgURL postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type VideosRefsTable struct {
videosRefsTable
EXCLUDED videosRefsTable
}
// AS creates new VideosRefsTable with assigned alias
func (a VideosRefsTable) AS(alias string) *VideosRefsTable {
return newVideosRefsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new VideosRefsTable with assigned schema name
func (a VideosRefsTable) FromSchema(schemaName string) *VideosRefsTable {
return newVideosRefsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new VideosRefsTable with assigned table prefix
func (a VideosRefsTable) WithPrefix(prefix string) *VideosRefsTable {
return newVideosRefsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new VideosRefsTable with assigned table suffix
func (a VideosRefsTable) WithSuffix(suffix string) *VideosRefsTable {
return newVideosRefsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newVideosRefsTable(schemaName, tableName, alias string) *VideosRefsTable {
return &VideosRefsTable{
videosRefsTable: newVideosRefsTableImpl(schemaName, tableName, alias),
EXCLUDED: newVideosRefsTableImpl("", "excluded", ""),
}
}
func newVideosRefsTableImpl(schemaName, tableName, alias string) videosRefsTable {
var (
CodiceColumn = postgres.StringColumn("codice")
OrdineColumn = postgres.IntegerColumn("ordine")
VideoColumn = postgres.StringColumn("video")
ThumbColumn = postgres.StringColumn("thumb")
OgURLColumn = postgres.StringColumn("og_url")
allColumns = postgres.ColumnList{CodiceColumn, OrdineColumn, VideoColumn, ThumbColumn, OgURLColumn}
mutableColumns = postgres.ColumnList{CodiceColumn, OrdineColumn, VideoColumn, ThumbColumn, OgURLColumn}
defaultColumns = postgres.ColumnList{}
)
return videosRefsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
Codice: CodiceColumn,
Ordine: OrdineColumn,
Video: VideoColumn,
Thumb: ThumbColumn,
OgURL: OgURLColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View file

@ -18,7 +18,7 @@ import (
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/google/uuid"
"github.com/nfnt/resize"
"github.com/nickalie/go-webpbin"
)
@ -101,12 +101,12 @@ func ProcessImagePool(toProcess *[]typesdefs.MediaToProcess) ([]typesdefs.Upload
defer uploadWg.Done()
defer func() { <-uploadSem }() // release semaphore
imgId, err := UploadFile(img.Path, "image/webp", "images")
imgId, err := queries.UploadFile(img.Path, "image/webp", "images")
if err != nil {
log.Printf("failed to upload image: %v", err)
return
}
thumbId, err := UploadFile(img.ThumnailPath, "image/webp", "images")
thumbId, err := queries.UploadFile(img.ThumnailPath, "image/webp", "images")
if err != nil {
log.Printf("failed to upload thumbnail: %v", err)
return
@ -272,28 +272,19 @@ func thumbnail_creation(img image.Image, output string, outputdir string) error
return nil
}
func FallbackImage(c echo.Context) error {
// Serve a fallback image if the requested image is not found
fallbackPath := "fallback.webp" // Path to your fallback image
if _, err := os.Stat(fallbackPath); os.IsNotExist(err) {
return c.String(http.StatusNotFound, "Fallback image not found")
}
return c.File(fallbackPath)
}
type imgRef struct {
Og_url string
Img string
Thumb string
Img uuid.UUID
Thumb uuid.UUID
}
func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) {
currentBucketState, err := ListBucket("images")
currentBucketState, err := queries.GetStorageByTag("images")
if err != nil {
return nil, fmt.Errorf("failed to list bucket: %w", err)
}
//make a list of bucket ids
bucketIdsMap := make(map[string]bool)
bucketIdsMap := make(map[uuid.UUID]bool)
for _, item := range currentBucketState {
bucketIdsMap[item.ID] = true
}
@ -306,7 +297,7 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) {
dbImagesMap := make(map[string][]imgRef)
for _, ref := range db_images {
dbImagesMap[ref.Codice] = append(dbImagesMap[ref.Codice], imgRef{
Og_url: ref.Og_url,
Og_url: ref.OgURL,
Img: ref.Img,
Thumb: ref.Thumb,
})
@ -351,7 +342,7 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) {
}
func ForceClearImages(codici []string) error {
var idsToDelete []string
var idsToDelete uuid.UUIDs
for _, codice := range codici {
refs, err := queries.GetCodiceImagesFromDB(codice)
if err != nil {
@ -363,7 +354,7 @@ func ForceClearImages(codici []string) error {
}
}
err := DeleteIdsFromStorage(idsToDelete)
err := queries.DeleteMultFile(idsToDelete)
if err != nil {
return fmt.Errorf("failed to delete images from storage: %w", err)
}
@ -378,12 +369,12 @@ func ForceClearImages(codici []string) error {
// Removes stranded images (in storage but not in DB)
func ReconcileImages() error {
currentBucketState, err := ListBucket("images")
currentBucketState, err := queries.GetStorageByTag("images")
if err != nil {
return fmt.Errorf("failed to list bucket: %w", err)
}
//make a list of bucket ids
bucketIdsMap := make(map[string]bool)
bucketIdsMap := make(map[uuid.UUID]bool)
for _, item := range currentBucketState {
bucketIdsMap[item.ID] = true
}
@ -391,13 +382,13 @@ func ReconcileImages() error {
if err != nil {
return fmt.Errorf("failed to get images from DB: %w", err)
}
knownStorageIds := make(map[string]bool)
knownStorageIds := make(map[uuid.UUID]bool)
for _, ref := range db_images {
knownStorageIds[ref.Img] = true
knownStorageIds[ref.Thumb] = true
}
// reconcile: delete storage files not referenced in DB
var strandedIds []string
var strandedIds uuid.UUIDs
for _, item := range currentBucketState {
if !knownStorageIds[item.ID] {
strandedIds = append(strandedIds, item.ID)
@ -405,7 +396,7 @@ func ReconcileImages() error {
}
if len(strandedIds) > 0 {
log.Printf("Reconciliation: found %d stranded files, deleting...", len(strandedIds))
if err := DeleteIdsFromStorage(strandedIds); err != nil {
if err := queries.DeleteMultFile(strandedIds); err != nil {
log.Printf("Warning: failed to delete stranded files: %v", err) // non-fatal
}
} else {

3
apps/backend/jet.sh Normal file
View file

@ -0,0 +1,3 @@
#!/bin/bash
jet -dsn=postgresql://postgres:rootpost@localhost:5433/postgres?sslmode=disable -schema=public -path=./gen/postgres -tables=storage,annunci,images_refs,videos_refs -ignore-enums=bantype,genericstatusenum,ordertypeenum,paymentstatusenum,statusconfermaenum,tipologiaposizioneenum

View file

@ -487,14 +487,14 @@ func processDatiImmobile(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.Annu
tmp.Numero_postiauto = annuncio.PostiAuto
}
func parseCaratteristiche(wrkrecord *typesdefs.AnnuncioXML, m *MiogestHandler) map[string]interface{} {
if reflect.TypeOf(wrkrecord).Kind() != reflect.Ptr || reflect.ValueOf(wrkrecord).Elem().Kind() != reflect.Struct {
func parseCaratteristiche(wrkrecord *typesdefs.AnnuncioXML, m *MiogestHandler) map[string]any {
if reflect.TypeFor[*typesdefs.AnnuncioXML]().Kind() != reflect.Pointer || reflect.ValueOf(wrkrecord).Elem().Kind() != reflect.Struct {
return nil
}
tmpCaratteristiche := make(map[string]interface{})
tmpCaratteristiche := make(map[string]any)
val := reflect.ValueOf(wrkrecord).Elem()
numField := val.NumField()
for i := 0; i < numField; i++ {
for i := range numField {
if strings.HasPrefix(val.Type().Field(i).Name, "Scheda_") {
nameField := val.Type().Field(i).Name
valueField := val.Field(i)

View file

@ -1,18 +1,22 @@
package queries
import (
models "backend/gen/postgres/postgres/public/model"
. "backend/gen/postgres/postgres/public/table"
"backend/typesdefs"
"fmt"
"slices"
"time"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
. "github.com/go-jet/jet/v2/postgres"
"github.com/lib/pq"
)
func AnnunciGetActive() ([]string, error) {
var annunci []string
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &annunci, "SELECT codice FROM public.annunci WHERE web = true and stato = 'Attivo' ")
if err != nil && err != pgx.ErrNoRows {
stmt := SELECT(Annunci.Codice).FROM(Annunci).WHERE(AND(Annunci.Web.IS_TRUE(), Annunci.Stato.EQ(Text("Attivo"))))
err := stmt.Query(postgres, &annunci)
if err != nil {
return nil, fmt.Errorf("failed to get active annunci: %w", err)
}
return annunci, nil
@ -22,7 +26,8 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e
var err error
if resetAllBeforeUpdate {
//SET ALL WEB TO FALSE
_, err = Db.Dbpool.Exec(Db.Ctx, "UPDATE public.annunci SET web = false, homepage = false, stato='Sospeso'")
stmt := Annunci.UPDATE(Annunci.Web, Annunci.Homepage, Annunci.Stato).SET(false, false, "Sospeso").WHERE(Annunci.Codice.IS_NOT_NULL())
_, err = stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to set all web to false: %w", err)
@ -30,164 +35,97 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e
}
//GET ALL CODICI
var codici []string
err = pgxscan.Select(Db.Ctx, Db.Dbpool, &codici, "SELECT DISTINCT codice FROM public.annunci")
stmt := SELECT(Annunci.Codice).DISTINCT(Annunci.Codice).FROM(Annunci)
err = stmt.Query(postgres, &codici)
if err != nil {
return fmt.Errorf("failed to get all codici: %w", err)
}
batch := &pgx.Batch{}
for _, annuncio := range annunci.Annuncio {
var to_update []models.Annunci
var to_insert []models.Annunci
for _, annuncio := range annunci.Annuncio {
value := models.Annunci{
Locatore: annuncio.Locatore,
Numero: annuncio.Numero,
Idlocatore: annuncio.Idlocatore,
Email: annuncio.Email,
CreatoIl: annuncio.Creato_il,
ModificatoIl: annuncio.Modificato_il,
Indirizzo: annuncio.Indirizzo,
Civico: annuncio.Civico,
Comune: annuncio.Comune,
Cap: annuncio.Cap,
Provincia: annuncio.Provincia,
Regione: annuncio.Regione,
Lat: annuncio.Lat,
Lon: annuncio.Lon,
IndirizzoSecondario: annuncio.Indirizzo_secondario,
CivicoSecondario: annuncio.Civico_secondario,
LatSecondario: annuncio.Lat_secondario,
LonSecondario: annuncio.Lon_secondario,
Tipo: annuncio.Tipo,
Categorie: (*pq.StringArray)(annuncio.Categorie),
Prezzo: int32(annuncio.Prezzo),
Anno: annuncio.Anno,
Consegna: intToint32Ptr(annuncio.Consegna),
Classe: annuncio.Classe,
Mq: annuncio.Mq,
Piano: annuncio.Piano,
PianoPalazzo: strPtrToInt32Ptr(annuncio.Piano_palazzo),
UnitaCondominio: strPtrToInt32Ptr(annuncio.Unita_condominio),
NumeroVani: strPtrToInt32Ptr(annuncio.Numero_vani),
NumeroCamere: strPtrToInt32Ptr(annuncio.Numero_camere),
NumeroBagni: strPtrToInt32Ptr(annuncio.Numero_bagni),
NumeroBalconi: strPtrToInt32Ptr(annuncio.Numero_balconi),
NumeroTerrazzi: strPtrToInt32Ptr(annuncio.Numero_terrazzi),
NumeroBox: strPtrToInt32Ptr(annuncio.Numero_box),
NumeroPostiauto: strPtrToInt32Ptr(annuncio.Numero_postiauto),
Caratteristiche: mapToJSONStrPtr(annuncio.Caratteristiche),
Accessori: (*pq.StringArray)(annuncio.Accessori),
TitoloIt: annuncio.Titolo_it,
DescIt: annuncio.Desc_it,
TitoloEn: annuncio.Titolo_en,
DescEn: annuncio.Desc_en,
Stato: annuncio.Stato,
Web: annuncio.Web,
Homepage: annuncio.Homepage,
ExternalVideos: (*pq.StringArray)(annuncio.External_videos),
Codice: annuncio.Codice,
DisponibileDa: strPtrToTimePtrSilent(annuncio.Disponibile_da),
Permanenza: intSliceToPQInt32Array(annuncio.Permanenza),
Persone: (*pq.StringArray)(annuncio.Persone),
TipoLocatore: annuncio.Tipo_locatore,
MediaUpdatedAt: Ptr(time.Now()),
}
if slices.Contains(codici, annuncio.Codice) {
batch.Queue(`
UPDATE public.annunci
SET locatore = $1,
numero = $2,
idlocatore = $3,
email = $4,
creato_il = $5,
modificato_il = $6,
indirizzo = $7,
civico = $8,
comune = $9,
cap = $10,
provincia = $11,
regione = $12,
lat = $13,
lon = $14,
indirizzo_secondario = $15,
civico_secondario = $16,
lat_secondario = $17,
lon_secondario = $18,
tipo = $19,
categorie = $20,
prezzo = $21,
anno = $22,
consegna = $23,
classe = $24,
mq = $25,
piano = $26,
piano_palazzo = $27,
unita_condominio = $28,
numero_vani = $29,
numero_camere = $30,
numero_bagni = $31,
numero_balconi = $32,
numero_terrazzi = $33,
numero_box = $34,
numero_postiauto = $35,
caratteristiche = $36,
accessori = $37,
titolo_it = $38,
desc_it = $39,
titolo_en = $40,
desc_en = $41,
stato = $42,
web = $43,
homepage = $44,
external_videos = $45,
disponibile_da = $47,
permanenza = $48,
persone = $49,
media_updated_at = NOW(),
tipo_locatore = $50
WHERE codice = $46
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,
annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il,
annuncio.Indirizzo, annuncio.Civico, annuncio.Comune, annuncio.Cap,
annuncio.Provincia, annuncio.Regione, annuncio.Lat, annuncio.Lon,
annuncio.Indirizzo_secondario, annuncio.Civico_secondario, annuncio.Lat_secondario,
annuncio.Lon_secondario, annuncio.Tipo, annuncio.Categorie, annuncio.Prezzo, annuncio.Anno,
annuncio.Consegna, annuncio.Classe, annuncio.Mq, annuncio.Piano, annuncio.Piano_palazzo,
annuncio.Unita_condominio, annuncio.Numero_vani, annuncio.Numero_camere, annuncio.Numero_bagni,
annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto,
annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en,
annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage,
annuncio.External_videos, annuncio.Codice, annuncio.Disponibile_da, annuncio.Permanenza, annuncio.Persone, annuncio.Tipo_locatore)
to_update = append(to_update, value)
} else {
batch.Queue(`
INSERT INTO public.annunci(
locatore,
numero,
idlocatore,
email,
creato_il,
modificato_il,
indirizzo,
civico,
comune,
cap,
provincia,
regione,
lat,
lon,
indirizzo_secondario,
civico_secondario,
lat_secondario,
lon_secondario,
tipo,
categorie,
prezzo,
anno,
consegna,
classe,
mq,
piano,
piano_palazzo,
unita_condominio,
numero_vani,
numero_camere,
numero_bagni,
numero_balconi,
numero_terrazzi,
numero_box,
numero_postiauto,
caratteristiche,
accessori,
titolo_it,
desc_it,
titolo_en,
desc_en,
stato,
web,
homepage,
external_videos,
codice,
disponibile_da,
permanenza,
persone,
media_updated_at,
tipo_locatore
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, NOW(), $50)
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,
annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il,
annuncio.Indirizzo, annuncio.Civico, annuncio.Comune, annuncio.Cap,
annuncio.Provincia, annuncio.Regione, annuncio.Lat, annuncio.Lon,
annuncio.Indirizzo_secondario, annuncio.Civico_secondario, annuncio.Lat_secondario,
annuncio.Lon_secondario, annuncio.Tipo, annuncio.Categorie, annuncio.Prezzo, annuncio.Anno,
annuncio.Consegna, annuncio.Classe, annuncio.Mq, annuncio.Piano, annuncio.Piano_palazzo,
annuncio.Unita_condominio, annuncio.Numero_vani, annuncio.Numero_camere, annuncio.Numero_bagni,
annuncio.Numero_balconi, annuncio.Numero_terrazzi, annuncio.Numero_box, annuncio.Numero_postiauto,
annuncio.Caratteristiche, annuncio.Accessori, annuncio.Titolo_it, annuncio.Desc_it, annuncio.Titolo_en,
annuncio.Desc_en, annuncio.Stato, annuncio.Web, annuncio.Homepage,
annuncio.External_videos, annuncio.Codice, annuncio.Disponibile_da, annuncio.Permanenza, annuncio.Persone, annuncio.Tipo_locatore)
to_insert = append(to_insert, value)
}
}
if len(to_insert) > 0 {
stmt := Annunci.INSERT(Annunci.MutableColumns).MODELS(to_insert)
_, err = stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to insert/update annunci batch: %w", err)
}
}
err = Db.Dbpool.SendBatch(Db.Ctx, batch).Close()
if len(to_update) > 0 {
for _, upd := range to_update {
stmt := Annunci.UPDATE(Annunci.MutableColumns).MODEL(upd).WHERE(Annunci.Codice.EQ(Text(upd.Codice)))
_, err = stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to insert/update annunci batch: %w", err)
}
}
}
if err != nil {
return fmt.Errorf("failed to insert/update annunci batch: %w", err)
}
fmt.Println("Annunci insert/update done")
return nil
}
func clearRecords() {
//SET ALL WEB TO FALSE
_, err := Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.annunci")
if err != nil {
panic(err)
}
}

View file

@ -1,12 +1,13 @@
package queries
import (
models "backend/gen/postgres/postgres/public/model"
. "backend/gen/postgres/postgres/public/table"
"backend/typesdefs"
"fmt"
"log"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
. "github.com/go-jet/jet/v2/postgres"
)
func SetImagesToDB(pool *[]typesdefs.UploadedMedia) error {
@ -14,43 +15,53 @@ func SetImagesToDB(pool *[]typesdefs.UploadedMedia) error {
log.Println("No new images to save to DB.")
return nil
}
batch := &pgx.Batch{}
var to_insert []models.ImagesRefs
for _, data := range *pool {
batch.Queue(`
INSERT INTO public.images_refs (codice, ordine, img, thumb, og_url)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (codice, ordine) DO UPDATE SET img = $3, thumb = $4
`, data.Codice, data.Order, data.MediaId, data.ThumbId, data.OGUrl)
}
if err := Db.Dbpool.SendBatch(Db.Ctx, batch).Close(); err != nil {
to_insert = append(to_insert, models.ImagesRefs{
Codice: data.Codice,
Ordine: int32(data.Order),
Img: data.MediaId,
Thumb: data.ThumbId,
OgURL: data.OGUrl,
})
}
stmt := ImagesRefs.INSERT(ImagesRefs.AllColumns).MODELS(to_insert).ON_CONFLICT(ImagesRefs.Codice, ImagesRefs.Ordine).DO_UPDATE(SET(ImagesRefs.Img.SET(ImagesRefs.EXCLUDED.Img), ImagesRefs.Thumb.SET(ImagesRefs.EXCLUDED.Thumb)))
_, err := stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to insert/update images batch: %w", err)
}
return nil
}
func GetImagesFromDB() ([]typesdefs.ImagesRefs, error) {
var images []typesdefs.ImagesRefs
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &images, "SELECT * FROM public.images_refs ORDER BY ordine ASC")
if err != nil && err != pgx.ErrNoRows {
func GetImagesFromDB() ([]models.ImagesRefs, error) {
var images []models.ImagesRefs
stmt := SELECT(ImagesRefs.AllColumns).FROM(ImagesRefs).ORDER_BY(ImagesRefs.Ordine.ASC())
err := stmt.Query(postgres, &images)
if err != nil {
return nil, fmt.Errorf("failed to get images: %w", err)
}
return images, nil
}
func GetCodiceImagesFromDB(codice string) ([]typesdefs.ImagesRefs, error) {
var images []typesdefs.ImagesRefs
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &images, "SELECT * FROM public.images_refs WHERE codice = $1 ORDER BY ordine ASC", codice)
if err != nil && err != pgx.ErrNoRows {
func GetCodiceImagesFromDB(codice string) ([]models.ImagesRefs, error) {
var images []models.ImagesRefs
stmt := SELECT(ImagesRefs.AllColumns).FROM(ImagesRefs).WHERE(ImagesRefs.Codice.EQ(Text(codice))).ORDER_BY(ImagesRefs.Ordine.ASC())
err := stmt.Query(postgres, &images)
if err != nil {
return nil, fmt.Errorf("failed to get images: %w", err)
}
return images, nil
}
func ClearImagesRefsByCodice(codice string) error {
_, err := Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.images_refs WHERE codice = $1", codice)
stmt := ImagesRefs.DELETE().WHERE(ImagesRefs.Codice.EQ(Text(codice)))
_, err := stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to clear images refs by codice: %w", err)
}

View file

@ -1,35 +1,34 @@
package queries
import (
"backend/db"
"database/sql"
"fmt"
"os"
"github.com/joho/godotenv"
)
var (
Db *db.DbHandler
postgres *sql.DB
)
func init() {
_ = godotenv.Load(".env")
Db = db.NewDbHandler(db.Configs{
Host: os.Getenv("PGHOST"),
Dbname: os.Getenv("POSTGRES_DB"),
Port: os.Getenv("PGPORT"),
User: os.Getenv("POSTGRES_USER"),
Password: os.Getenv("POSTGRES_PASSWORD"),
LoggerEnabled: os.Getenv("DB_LOGGER") == "true",
})
var err error
connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", os.Getenv("POSTGRES_USER"), os.Getenv("POSTGRES_PASSWORD"), os.Getenv("PGHOST"), os.Getenv("PGPORT"), os.Getenv("POSTGRES_DB"))
postgres, err = sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
}
func HealthCheck() bool {
if Db == nil || Db.Dbpool == nil {
if postgres == nil {
return false
}
err := Db.Dbpool.Ping(Db.Ctx)
err := postgres.Ping()
return err == nil
}

View file

@ -0,0 +1,92 @@
package queries
import (
models "backend/gen/postgres/postgres/public/model"
. "backend/gen/postgres/postgres/public/table"
"fmt"
"io"
"os"
"path/filepath"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
// func DeleteFile(id uuid.UUID) error {
// stmt := Storage.DELETE().WHERE(Storage.ID.EQ(UUID(id)))
// _, err := stmt.Exec(postgres)
// if err != nil {
// return fmt.Errorf("failed to delete file: %w", err)
// }
// return nil
// }
func DeleteMultFile(ids uuid.UUIDs) error {
if len(ids) == 0 {
return nil
}
var sqlIDs []Expression
for _, id := range ids {
sqlIDs = append(sqlIDs, UUID(id))
}
stmt := Storage.DELETE().WHERE(Storage.ID.IN(sqlIDs...))
_, err := stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to delete files: %w", err)
}
return nil
}
func GetStorageByTag(tag string) ([]models.Storage, error) {
var rows []models.Storage
stmt := SELECT(Storage.AllColumns).FROM(Storage).WHERE(Storage.Tag.EQ(Text(tag)))
err := stmt.Query(postgres, &rows)
if err != nil {
return nil, fmt.Errorf("failed to get storage by tag: %w", err)
}
return rows, nil
}
func UploadFile(path string, mimeType string, tag string) (uuid.UUID, error) {
file, err := os.Open(path)
if err != nil {
return uuid.UUID{}, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
bytes, err := io.ReadAll(file)
if err != nil {
return uuid.UUID{}, fmt.Errorf("failed to read file: %w", err)
}
to_insert := models.Storage{
MimeType: mimeType,
Tag: &tag,
FileData: bytes,
Filename: filepath.Base(file.Name()),
FileSizeBytes: int32(len(bytes)),
}
stmt := Storage.INSERT(Storage.MimeType,
Storage.Tag,
Storage.FileData,
Storage.Filename,
Storage.FileSizeBytes).MODEL(to_insert).RETURNING(Storage.ID)
var newIdStrings []string
err = stmt.Query(postgres, &newIdStrings)
if err != nil {
return uuid.UUID{}, fmt.Errorf("failed to insert file into DB: %w", err)
}
if len(newIdStrings) == 0 {
return uuid.UUID{}, fmt.Errorf("no ID was returned from insert")
}
actualUUID, err := uuid.Parse(newIdStrings[0])
if err != nil {
return uuid.UUID{}, fmt.Errorf("failed to parse UUID: %w", err)
}
return actualUUID, nil
}

View file

@ -0,0 +1,72 @@
package queries
import (
"encoding/json"
"strconv"
"time"
"github.com/lib/pq"
)
func intToint32Ptr(i *int) *int32 {
if i == nil {
return nil
}
v := int32(*i)
return &v
}
func Ptr[T any](v T) *T {
return &v
}
func strPtrToInt32Ptr(s *string) *int32 {
if s == nil {
return nil
}
val, err := strconv.ParseInt(*s, 10, 32)
if err != nil {
return nil
}
result := int32(val)
return &result
}
func intSliceToPQInt32Array(v *[]int) *pq.Int32Array {
if v == nil {
return nil
}
// Create the pq.Int32Array with the correct capacity
arr := make(pq.Int32Array, len(*v))
for i, val := range *v {
arr[i] = int32(val)
}
return &arr
}
func mapToJSONStrPtr(m *map[string]interface{}) *string {
if m == nil {
return nil
}
bytes, err := json.Marshal(m)
if err != nil {
return nil
}
str := string(bytes)
return &str
}
func strPtrToTimePtrSilent(s *string) *time.Time {
if s == nil {
return nil
}
parsedTime, err := time.Parse(time.DateOnly, *s)
if err != nil {
return nil // Silently returns NULL to the DB if parsing fails
}
return &parsedTime
}

View file

@ -1,12 +1,13 @@
package queries
import (
models "backend/gen/postgres/postgres/public/model"
. "backend/gen/postgres/postgres/public/table"
"backend/typesdefs"
"fmt"
"log"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
. "github.com/go-jet/jet/v2/postgres"
)
func SetVideosToDB(pool *[]typesdefs.UploadedMedia) error {
@ -14,43 +15,49 @@ func SetVideosToDB(pool *[]typesdefs.UploadedMedia) error {
log.Println("No new videos to save to DB.")
return nil
}
batch := &pgx.Batch{}
var to_insert []models.VideosRefs
for _, data := range *pool {
batch.Queue(`
INSERT INTO public.videos_refs (codice, ordine, video, thumb, og_url)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (codice, ordine) DO UPDATE SET video = $3, thumb = $4
`, data.Codice, data.Order, data.MediaId, data.ThumbId, data.OGUrl)
}
to_insert = append(to_insert, models.VideosRefs{
Codice: data.Codice,
Ordine: int32(data.Order),
Video: data.MediaId,
Thumb: data.ThumbId,
OgURL: data.OGUrl,
})
if err := Db.Dbpool.SendBatch(Db.Ctx, batch).Close(); err != nil {
return fmt.Errorf("failed to insert/update video batch: %w", err)
}
stmt := VideosRefs.INSERT(VideosRefs.AllColumns).MODELS(to_insert).ON_CONFLICT(VideosRefs.Codice, VideosRefs.Ordine).DO_UPDATE(SET(VideosRefs.Video.SET(VideosRefs.EXCLUDED.Video), VideosRefs.Thumb.SET(VideosRefs.EXCLUDED.Thumb)))
_, err := stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to insert video refs: %w", err)
}
return nil
}
func GetVideosFromDB() ([]typesdefs.VideosRefs, error) {
var videos []typesdefs.VideosRefs
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &videos, "SELECT * FROM public.videos_refs ORDER BY ordine ASC")
if err != nil && err != pgx.ErrNoRows {
func GetVideosFromDB() ([]models.VideosRefs, error) {
var videos []models.VideosRefs
stmt := VideosRefs.SELECT(VideosRefs.AllColumns).FROM(VideosRefs).ORDER_BY(VideosRefs.Ordine.ASC())
err := stmt.Query(postgres, &videos)
if err != nil {
return nil, fmt.Errorf("failed to get videos: %w", err)
}
return videos, nil
}
func GetCodiceVideosFromDB(codice string) ([]typesdefs.VideosRefs, error) {
var videos []typesdefs.VideosRefs
err := pgxscan.Select(Db.Ctx, Db.Dbpool, &videos, "SELECT * FROM public.videos_refs WHERE codice = $1 ORDER BY ordine ASC", codice)
if err != nil && err != pgx.ErrNoRows {
func GetCodiceVideosFromDB(codice string) ([]models.VideosRefs, error) {
var videos []models.VideosRefs
stmt := VideosRefs.SELECT(VideosRefs.AllColumns).FROM(VideosRefs).WHERE(VideosRefs.Codice.EQ(Text(codice))).ORDER_BY(VideosRefs.Ordine.ASC())
err := stmt.Query(postgres, &videos)
if err != nil {
return nil, fmt.Errorf("failed to get videos: %w", err)
}
return videos, nil
}
func ClearVideosRefsByCodice(codice string) error {
_, err := Db.Dbpool.Exec(Db.Ctx, "DELETE FROM public.videos_refs WHERE codice = $1", codice)
stmt := VideosRefs.DELETE().WHERE(VideosRefs.Codice.EQ(Text(codice)))
_, err := stmt.Exec(postgres)
if err != nil {
return fmt.Errorf("failed to clear videos refs by codice: %w", err)
}

View file

@ -61,14 +61,10 @@ func (s *Server) SetupRoutes() *echo.Echo {
})
})
e.GET("/test", func(c echo.Context) error {
files, err := ListBucket("images")
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
return c.JSONPretty(http.StatusOK,
map[string]any{
"files": files,
"files": "",
}, " ")
})
e.POST("/testpost", func(c echo.Context) error {
@ -79,15 +75,7 @@ func (s *Server) SetupRoutes() *echo.Echo {
})
e.GET("/health", func(c echo.Context) error {
h := queries.HealthCheck()
s_error := HealthCheckStorage()
var s string
if s_error != nil {
s = s_error.Error()
} else {
s = "OK"
}
return c.JSONPretty(http.StatusOK, fmt.Sprintf("DB Connection: %v, Storage Connection: %v", h, s), " ")
return c.JSONPretty(http.StatusOK, fmt.Sprintf("DB Connection: %v", h), " ")
})
// MIOGEST ROUTES

View file

@ -1,223 +0,0 @@
package main
import (
"backend/config"
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"path/filepath"
"time"
)
func HealthCheckStorage() error {
resp, err := http.Get(config.Cfg.Storage.Endpoint + "/health")
if err != nil {
return fmt.Errorf("storage health check failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("storage health check failed: %s", resp.Status)
}
return nil
}
func DeleteFile(id string) error {
req, err := http.NewRequest("DELETE", config.Cfg.Storage.Endpoint+"/delete/"+id+"?token="+config.Cfg.Storage.Token, nil)
if err != nil {
return fmt.Errorf("failed to create delete request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("delete request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("delete failed: %s", resp.Status)
}
return nil
}
func DeleteIdsFromStorage(ids []string) error {
for _, id := range ids {
err := DeleteFile(id)
if err != nil {
return fmt.Errorf("failed to delete file with id %s: %w", id, err)
}
}
return nil
}
func GetFile(path string) ([]byte, error) {
panic("unimplemented")
}
type FileMetadata struct {
ID string `json:"id"`
OriginalName string `json:"originalName"`
Size int64 `json:"size"`
MIMEType string `json:"mimeType"`
BlockCount int `json:"blockCount"`
UploadedAt time.Time `json:"uploadedAt"`
ExpiresAt *time.Time `json:"expiresAt"`
Bucket string `json:"bucket,omitempty"`
}
// func ListFiles() ([]FileMetadata, error) {
// //get list of files from api
// resp, err := http.Get(config.Cfg.Storage.Endpoint + "/files?token=" + config.Cfg.Storage.Token)
// if err != nil {
// return nil, err
// }
// defer resp.Body.Close()
// if resp.StatusCode != http.StatusOK {
// return nil, fmt.Errorf("failed to list files: %s", resp.Status)
// }
// //parse response
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// fmt.Println("Error reading response body:", err)
// return nil, err
// }
// var data []FileMetadata
// err = json.Unmarshal(body, &data)
// if err != nil {
// fmt.Println("Error unmarshalling response body:", err)
// return nil, err
// }
// return data, nil
// }
func ListBucket(bucketName string) ([]FileMetadata, error) {
//get list of files from api
resp, err := http.Get(config.Cfg.Storage.Endpoint + "/bucket/" + bucketName + "?token=" + config.Cfg.Storage.Token)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to list files: %s", resp.Status)
}
//parse response
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return nil, err
}
var data []FileMetadata
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Println("Error unmarshalling response body:", err)
return nil, err
}
return data, nil
}
// type CheckFileResponse struct {
// Available []FileMetadata `json:"available"`
// Unavailable []string `json:"unavailable"`
// }
// func CheckFiles(ids []string) ([]FileMetadata, []string, error) {
// //application/json with array of ids as body of post request to /files-check
// jsonData, err := json.Marshal(ids)
// if err != nil {
// return nil, nil, err
// }
// resp, err := http.Post(config.Cfg.Storage.Endpoint+"/files-check?token="+config.Cfg.Storage.Token,
// "application/json", bytes.NewBuffer(jsonData))
// if err != nil {
// return nil, nil, err
// }
// defer resp.Body.Close()
// if resp.StatusCode != http.StatusOK {
// return nil, nil, fmt.Errorf("failed to check files: %s", resp.Status)
// }
// //parse response
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// fmt.Println("Error reading response body:", err)
// return nil, nil, err
// }
// var data CheckFileResponse
// err = json.Unmarshal(body, &data)
// if err != nil {
// fmt.Println("Error unmarshalling response body:", err)
// return nil, nil, err
// }
// return data.Available, data.Unavailable, nil
// }
func UploadFile(path string, mimeType string, bucket string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filepath.Base(path)))
h.Set("Content-Type", mimeType)
part, err := writer.CreatePart(h)
if err != nil {
return "", fmt.Errorf("failed to create form file: %w", err)
}
if _, err := io.Copy(part, file); err != nil {
return "", fmt.Errorf("failed to copy file data: %w", err)
}
err = writer.WriteField("bucket", bucket)
if err != nil {
return "", fmt.Errorf("failed to write bucket field: %w", err)
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("failed to close writer: %w", err)
}
req, err := http.NewRequest("POST", config.Cfg.Storage.Endpoint+"/upload?token="+config.Cfg.Storage.Token, &body)
if err != nil {
return "", fmt.Errorf("failed to create upload request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("upload request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("upload failed: %s", resp.Status)
}
//parse response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read upload response: %w", err)
}
var data map[string]string
err = json.Unmarshal(respBody, &data)
if err != nil {
return "", fmt.Errorf("failed to parse upload response: %w", err)
}
if id, ok := data["id"]; ok {
return id, nil
}
return "", nil
}

View file

@ -1,8 +1,9 @@
package typesdefs
import (
"os"
"time"
"github.com/google/uuid"
)
type AnnunciXML struct {
@ -186,67 +187,6 @@ type AnnuncioParsed struct {
Persone *[]string
}
type AnnunciDB struct {
Id int
Codice string
Locatore *string
Tipo_locatore *string
Numero *string
Idlocatore *string
Indirizzo *string
Civico *string
Comune *string
Cap *string
Provincia *string
Regione *string
Lat *string
Lon *string
Indirizzo_secondario *string
Civico_secondario *string
Lat_secondario *string
Lon_secondario *string
Tipo *string
Categorie *[]string
Prezzo int
Anno *string
Consegna *string
Classe *string
Mq *float64
Piano *string
Piano_palazzo *int
Unita_condominio *int
Numero_vani *int
Numero_camere *int
Numero_bagni *int
Numero_balconi *int
Numero_terrazzi *int
Numero_box *int
Numero_postiauto *int
Accessori *[]string
Titolo_it *string
Desc_it *string
Titolo_en *string
Desc_en *string
Stato *string
Web *bool
Caratteristiche *map[string]interface{}
Homepage *bool
External_videos *[]string
Email *string
Creato_il *time.Time
Modificato_il *time.Time
Disponibile_da *time.Time
Permanenza *[]int
Persone *[]string
}
type File struct {
Name string
Size int64
Mode os.FileMode
ModTime string
IsDir bool
}
type DefaultCaratteristiche struct {
Scheda []struct {
Id string `xml:"id"`
@ -273,22 +213,6 @@ type Categoria struct {
Nome string
}
type ImagesRefs struct {
Codice string
Ordine int
Img string
Thumb string
Og_url string
}
type VideosRefs struct {
Codice string
Ordine int
Video string
Thumb string
Og_url string
}
type MediaToProcess struct {
Codice string
Url string
@ -306,7 +230,7 @@ type ProcessedMedia struct {
type UploadedMedia struct {
Codice string
Order int
MediaId string
ThumbId string
MediaId uuid.UUID
ThumbId uuid.UUID
OGUrl string
}

View file

@ -22,24 +22,24 @@ func MakeUppercase(s string) string {
return ""
}
func fetchXML(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return []byte{}, fmt.Errorf("GET error: %v", err)
}
defer resp.Body.Close()
// func fetchXML(url string) ([]byte, error) {
// resp, err := http.Get(url)
// if err != nil {
// return []byte{}, fmt.Errorf("GET error: %v", err)
// }
// defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return []byte{}, fmt.Errorf("status error: %v", resp.StatusCode)
}
// if resp.StatusCode != http.StatusOK {
// return []byte{}, fmt.Errorf("status error: %v", resp.StatusCode)
// }
data, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, fmt.Errorf("read body: %v", err)
}
// data, err := io.ReadAll(resp.Body)
// if err != nil {
// return []byte{}, fmt.Errorf("read body: %v", err)
// }
return data, nil
}
// return data, nil
// }
func GetXMLFromFile[T any](filePath string) (T, error) {
var result T
@ -144,11 +144,3 @@ func GetStringValue(strPtr *string) string {
func RemoveWhitespace(str string) string {
return strings.ReplaceAll(str, " ", "")
}
func CommaToDot(str *string) *string {
if str != nil {
result := strings.ReplaceAll(*str, ",", ".")
return &result
}
return nil
}

View file

@ -17,7 +17,7 @@ import (
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/google/uuid"
"github.com/nfnt/resize"
"github.com/nickalie/go-webpbin"
)
@ -111,12 +111,12 @@ func ProcessVideoPool(toProcess *[]typesdefs.MediaToProcess) ([]typesdefs.Upload
defer uploadWg.Done()
defer func() { <-uploadSem }() // release semaphore
videoId, err := UploadFile(video.Path, "video/mp4", "videos")
videoId, err := queries.UploadFile(video.Path, "video/mp4", "videos")
if err != nil {
log.Printf("failed to upload video: %v", err)
return
}
thumbId, err := UploadFile(video.ThumnailPath, "image/webp", "videos")
thumbId, err := queries.UploadFile(video.ThumnailPath, "image/webp", "videos")
if err != nil {
log.Printf("failed to upload thumbnail: %v", err)
return
@ -324,29 +324,20 @@ func extractThumbnail(input, output string) error {
return nil
}
func FallbackVideo(c echo.Context) error {
// Serve a fallback image if the requested image is not found
fallbackPath := "fallback.webp" // Path to your fallback image
if _, err := os.Stat(fallbackPath); os.IsNotExist(err) {
return c.String(http.StatusNotFound, "Fallback image not found")
}
return c.File(fallbackPath)
}
type videRef struct {
Og_url string
Video string
Thumb string
Video uuid.UUID
Thumb uuid.UUID
}
func CodiciToProcessVideos(annunci *typesdefs.AnnunciXML) ([]string, error) {
currentBucketState, err := ListBucket("videos")
currentBucketState, err := queries.GetStorageByTag("videos")
if err != nil {
return nil, fmt.Errorf("failed to list bucket: %w", err)
}
//make a list of bucket ids
bucketIdsMap := make(map[string]bool)
bucketIdsMap := make(map[uuid.UUID]bool)
for _, item := range currentBucketState {
bucketIdsMap[item.ID] = true
}
@ -359,7 +350,7 @@ func CodiciToProcessVideos(annunci *typesdefs.AnnunciXML) ([]string, error) {
dbVideosMap := make(map[string][]videRef) // Convert slice to map for faster lookup
for _, ref := range db_videos {
dbVideosMap[ref.Codice] = append(dbVideosMap[ref.Codice], videRef{
Og_url: ref.Og_url,
Og_url: ref.OgURL,
Video: ref.Video,
Thumb: ref.Thumb,
})
@ -422,7 +413,7 @@ func CodiciToProcessVideos(annunci *typesdefs.AnnunciXML) ([]string, error) {
}
func ForceClearVideos(codici []string) error {
var idsToDelete []string
var idsToDelete uuid.UUIDs
for _, codice := range codici {
refs, err := queries.GetCodiceVideosFromDB(codice)
if err != nil {
@ -434,7 +425,7 @@ func ForceClearVideos(codici []string) error {
}
}
err := DeleteIdsFromStorage(idsToDelete)
err := queries.DeleteMultFile(idsToDelete)
if err != nil {
return fmt.Errorf("failed to delete videos from storage: %w", err)
}
@ -449,12 +440,12 @@ func ForceClearVideos(codici []string) error {
// ReconcileVideos removes stranded videos (in storage but not in DB)
func ReconcileVideos() error {
currentBucketState, err := ListBucket("videos")
currentBucketState, err := queries.GetStorageByTag("videos")
if err != nil {
return fmt.Errorf("failed to list bucket: %w", err)
}
//make a list of bucket ids
bucketIdsMap := make(map[string]bool)
bucketIdsMap := make(map[uuid.UUID]bool)
for _, item := range currentBucketState {
bucketIdsMap[item.ID] = true
}
@ -462,7 +453,7 @@ func ReconcileVideos() error {
if err != nil {
return fmt.Errorf("failed to get videos from DB: %w", err)
}
var strandedIds []string
var strandedIds uuid.UUIDs
for _, ref := range db_videos {
if !bucketIdsMap[ref.Video] {
strandedIds = append(strandedIds, ref.Video)
@ -472,7 +463,7 @@ func ReconcileVideos() error {
}
}
if len(strandedIds) > 0 {
err := DeleteIdsFromStorage(strandedIds)
err := queries.DeleteMultFile(strandedIds)
if err != nil {
return fmt.Errorf("failed to delete stranded videos from storage: %w", err)
}

View file

@ -0,0 +1,190 @@
CREATE TABLE IF NOT EXISTS public.storage (
id uuid NOT NULL DEFAULT uuidv7(),
created_at timestamp without time zone NOT NULL DEFAULT now(),
filename text NOT NULL,
mime_type text NOT NULL,
file_size_bytes INT NOT NULL,
file_data BYTEA NOT NULL,
expires_at timestamp without time zone,
tag text
);
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'storage'
AND column_name = 'file_data'
) THEN
ALTER TABLE public.storage
ALTER COLUMN file_data SET STORAGE EXTERNAL;
END IF;
END $$;
DO $$ BEGIN IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'storage_pkey'
AND conrelid = 'public.storage'::regclass
) THEN
ALTER TABLE public.storage
ADD CONSTRAINT storage_pkey PRIMARY KEY (id);
END IF;
END $$;
--- users_storage
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'users_storage'
) THEN
TRUNCATE TABLE users_storage CASCADE;
END IF;
END $$;
ALTER TABLE IF EXISTS public.users_storage
DROP COLUMN IF EXISTS "storageId";
ALTER TABLE IF EXISTS public.users_storage
ADD COLUMN IF NOT EXISTS storage_id uuid;
DO $$ BEGIN IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'userstorage_storage'
AND conrelid = 'public.users_storage'::regclass
) THEN
ALTER TABLE public.users_storage
ADD CONSTRAINT userstorage_storage
FOREIGN KEY (storage_id) REFERENCES public.storage (id)
ON UPDATE CASCADE ON DELETE CASCADE;
END IF;
END $$;
ALTER TABLE users_storage
DROP CONSTRAINT IF EXISTS users_storage_user_storage_key;
DO $$ BEGIN IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'users_storage_user_storage_key'
) THEN
ALTER TABLE public.users_storage
ADD CONSTRAINT users_storage_user_storage_key UNIQUE ("userId", storage_id);
END IF;
END $$;
---IMAGES
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'images_refs'
) THEN
TRUNCATE TABLE images_refs;
END IF;
END $$;
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'images_refs'
AND column_name = 'img'
) THEN
ALTER TABLE images_refs
ALTER COLUMN img TYPE uuid USING img::uuid;
END IF;
END $$;
DO $$ BEGIN IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'images_refs_img_storage'
AND conrelid = 'public.images_refs'::regclass
) THEN
ALTER TABLE public.images_refs
ADD CONSTRAINT images_refs_img_storage
FOREIGN KEY (img) REFERENCES public.storage (id)
ON UPDATE CASCADE ON DELETE CASCADE;
END IF;
END $$;
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'images_refs'
AND column_name = 'thumb'
) THEN
ALTER TABLE images_refs
ALTER COLUMN thumb TYPE uuid USING thumb::uuid;
END IF;
END $$;
DO $$ BEGIN IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'images_refs_thumb_storage'
AND conrelid = 'public.images_refs'::regclass
) THEN
ALTER TABLE public.images_refs
ADD CONSTRAINT images_refs_thumb_storage
FOREIGN KEY (thumb) REFERENCES public.storage (id)
ON UPDATE CASCADE ON DELETE CASCADE;
END IF;
END $$;
--- VIDEOS
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'videos_refs'
) THEN
TRUNCATE TABLE videos_refs;
END IF;
END $$;
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'videos_refs'
AND column_name = 'video'
) THEN
ALTER TABLE videos_refs
ALTER COLUMN video TYPE uuid USING video::uuid;
END IF;
END $$;
DO $$ BEGIN IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'videos_refs_video_storage'
AND conrelid = 'public.videos_refs'::regclass
) THEN
ALTER TABLE public.videos_refs
ADD CONSTRAINT videos_refs_video_storage
FOREIGN KEY (video) REFERENCES public.storage (id)
ON UPDATE CASCADE ON DELETE CASCADE;
END IF;
END $$;
DO $$ BEGIN IF EXISTS (
SELECT
FROM information_schema.columns
WHERE table_name = 'videos_refs'
AND column_name = 'thumb'
) THEN
ALTER TABLE videos_refs
ALTER COLUMN thumb TYPE uuid USING thumb::uuid;
END IF;
END $$;
DO $$ BEGIN IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'videos_refs_thumb_storage'
AND conrelid = 'public.videos_refs'::regclass
) THEN
ALTER TABLE public.videos_refs
ADD CONSTRAINT videos_refs_thumb_storage
FOREIGN KEY (thumb) REFERENCES public.storage (id)
ON UPDATE CASCADE ON DELETE CASCADE;
END IF;
END $$;

View file

@ -75,11 +75,6 @@ ENV SKEBBY_USER=$SKEBBY_USER
ARG SKEBBY_PASS
ENV SKEBBY_PASS=$SKEBBY_PASS
ENV REVALIDATION_SECRET="SWKpgaaLfsyeqV1eOT0WG7TUFBewir8kJXure3O37ki8Lt4Z4IiEgBW4zPHDdM5c"
ARG STORAGE_URL
ENV STORAGE_URL=$STORAGE_URL
ARG STORAGE_TOKEN
ENV STORAGE_TOKEN=$STORAGE_TOKEN
RUN apk add --no-cache curl typst

View file

@ -12,6 +12,7 @@ TODOS:
- pre attivazione se admin mostrare tastino contatti in preview
- onboarding submit (toast e email che invitano a pagare)
- migliorare onboarding e pag guida
- rendere opzionale email e numero?
AFTER MVP:
- TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app

View file

@ -1,21 +0,0 @@
services:
storage:
image: "ghcr.io/marcopedone/go_fs:latest"
ports:
- "8080:8080"
environment:
PORT: 8080
AUTH_TOKEN: ${STORAGE_TOKEN}
pull_policy: "always"
volumes:
- storage_data:/app/storage
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
storage_data:

View file

@ -3,5 +3,3 @@
# Start the pgAdmin service using its Docker Compose file
docker compose -p pgadmin -f pgadmin-docker-compose.yml up -d
# Start the KeyDB service using its Docker Compose file
docker compose -p infoalloggi-dev -f dev-docker-compose.yml up -d

View file

@ -23,7 +23,6 @@
"ignoreDependencies": [
"pdfjs-dist",
"kysely-plugin-serialize",
"sharp",
"@react-email/components",
"kanel-kysely",
"kanel-zod",

View file

@ -17,51 +17,31 @@ async function createNextConfig(): Promise<NextConfig> {
await jiti.import("./src/env.ts");
const buildId = `${Date.now().toString(36)}`;
const apiVersion = `v.${buildId}`;
const configs: NextConfig = {
compiler: {
removeConsole: false,
},
experimental: {
scrollRestoration: true,
proxyClientMaxBodySize: "300mb",
// esmExternals: "loose", // This if react-pdf gives compilation issues
},
allowedDevOrigins: ["host.docker.internal"],
generateBuildId: async () => buildId,
devIndicators: false,
headers: async () => {
return [
{
source: "/:path*",
headers: [
{
key: "X-Frame-Options",
value: "",
},
],
},
{
source: "/api/trpc/:path*",
headers: [
{
key: "Cache-Control",
value:
"no-cache, no-store, must-revalidate, max-age=0, proxy-revalidate",
},
{
key: "X-API-Version",
value: apiVersion,
},
{
key: "X-Build-ID",
value: buildId,
},
],
},
];
},
// headers: async () => {
// return [
// {
// source: "/api/trpc/:path*",
// headers: [
// // {
// // key: "Cache-Control",
// // value:
// // "no-cache, no-store, must-revalidate, max-age=0, proxy-revalidate",
// // },
// ],
// },
// ];
// },
i18n: {
defaultLocale: "it",
locales: ["it", "en"],
@ -70,17 +50,6 @@ async function createNextConfig(): Promise<NextConfig> {
images: {
unoptimized: true,
minimumCacheTTL: 7200,
remotePatterns: [
{
hostname: "*.googleusercontent.com",
pathname: "**",
port: "",
protocol: "https",
},
{
hostname: `${env.STORAGE_URL}`,
},
],
},
output: env.NODE_ENV === "production" ? "standalone" : undefined, // This is for docker builds
transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"],

View file

@ -81,7 +81,6 @@
"react-reverse-portal": "^2.3.0",
"react-select": "^5.10.2",
"react-use": "^17.6.0",
"sharp": "^0.34.5",
"stripe": "^22.1.1",
"superjson": "^2.2.6",
"tailwind-merge": "^3.6.0",
@ -1376,6 +1375,7 @@
"node_modules/@img/colour": {
"version": "1.0.0",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
@ -1387,6 +1387,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1409,6 +1410,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1431,6 +1433,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1447,6 +1450,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1463,6 +1467,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1479,6 +1484,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1495,6 +1501,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1511,6 +1518,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1527,6 +1535,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1543,6 +1552,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1559,6 +1569,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1575,6 +1586,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1591,6 +1603,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1613,6 +1626,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1635,6 +1649,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1657,6 +1672,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1679,6 +1695,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1701,6 +1718,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1723,6 +1741,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1745,6 +1764,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -1767,6 +1787,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
@ -1786,6 +1807,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1805,6 +1827,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@ -1822,6 +1845,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@ -5776,6 +5800,29 @@
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
@ -12459,6 +12506,7 @@
},
"node_modules/semver": {
"version": "7.7.4",
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@ -12521,6 +12569,7 @@
"version": "0.34.5",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",

View file

@ -96,7 +96,6 @@
"react-reverse-portal": "^2.3.0",
"react-select": "^5.10.2",
"react-use": "^17.6.0",
"sharp": "^0.34.5",
"stripe": "^22.1.1",
"superjson": "^2.2.6",
"tailwind-merge": "^3.6.0",

View file

@ -36,7 +36,7 @@ export const AllegatoIframe = ({
setIsIOS(/iphone|ipad|ipod/i.test(userAgent));
// Try to detect if file is PDF from extension
setIsPDF(allegato.mimeType === "application/pdf");
setIsPDF(allegato.mime_type === "application/pdf");
}, [allegato]);
const handleIframeLoad = () => {
@ -52,7 +52,7 @@ export const AllegatoIframe = ({
};
const requestUrl = getStorageUrl({
storageId: allegato.id,
id: allegato.id,
params: { mode: "inline" },
});
// For PDF on iOS, provide download link instead

View file

@ -134,7 +134,7 @@ const SelectedComp = memo(
className="size-24 rounded-md object-cover sm:size-40"
height={500}
src={getStorageUrl({
storageId: selected.images[0].img,
id: selected.images[0].img,
params: {
media: "image",
cacheKey: selected.media_updated_at?.toISOString(),

View file

@ -151,7 +151,6 @@ export const CardAnnuncio = ({
</Link>
);
};
return (
<div
className={cn(
@ -219,7 +218,7 @@ export const CardAnnuncio = ({
<Image
alt={t.card.alt_immagine}
blurDataURL={getStorageUrl({
storageId: img.thumb,
id: img.thumb,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
@ -232,7 +231,7 @@ export const CardAnnuncio = ({
height={1080}
priority={idx === 0}
src={getStorageUrl({
storageId: img.img,
id: img.img,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
@ -255,7 +254,7 @@ export const CardAnnuncio = ({
<VideoPlayer
className="h-64 object-cover"
coverImage={getStorageUrl({
storageId: video.thumb,
id: video.thumb,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
@ -263,7 +262,7 @@ export const CardAnnuncio = ({
})}
key={`videoplayer-${video}`}
videoSrc={getStorageUrl({
storageId: video.video,
id: video.video,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "video",
@ -301,7 +300,7 @@ export const CardAnnuncio = ({
<Image
alt={t.card.alt_immagine}
blurDataURL={getStorageUrl({
storageId: images[0].thumb,
id: images[0].thumb,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
@ -311,7 +310,7 @@ export const CardAnnuncio = ({
height={1080}
priority={true}
src={getStorageUrl({
storageId: images[0].img,
id: images[0].img,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
@ -454,17 +453,17 @@ export const CarouselAnnuncio = ({
"aspect-square max-h-92 w-full rounded-md bg-[#e6e9ec] object-cover sm:max-h-80 xl:max-h-100",
immagini.length === 1 && "object-contain",
)}
height={400}
height={500}
onClick={() => handleOpenModal(idx)}
priority={idx === 0}
src={getStorageUrl({
storageId: img.img,
id: img.img,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
},
})}
width={800}
width={500}
/>
<Maximize2
className="absolute right-2 bottom-2 size-7 origin-bottom-right cursor-pointer rounded-md bg-muted-foreground/60 stroke-2 stroke-muted p-1 transition-all duration-300 ease-in-out hover:scale-150 group-hover:opacity-100 sm:opacity-0"
@ -486,7 +485,7 @@ export const CarouselAnnuncio = ({
<VideoPlayer
className="aspect-square max-h-92 object-cover sm:max-h-80 xl:max-h-100"
coverImage={getStorageUrl({
storageId: video.thumb,
id: video.thumb,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
@ -497,7 +496,7 @@ export const CarouselAnnuncio = ({
idx
}-${openModal}`}
videoSrc={getStorageUrl({
storageId: video.video,
id: video.video,
params: {
cacheKey: updated_at?.toISOString(),
media: "video",
@ -544,7 +543,7 @@ export const CarouselAnnuncio = ({
className="mx-auto h-[90vh] w-full rounded-md bg-[#e6e9ec] object-contain sm:w-[80vw]"
height={1080}
src={getStorageUrl({
storageId: img.img,
id: img.img,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
@ -563,14 +562,14 @@ export const CarouselAnnuncio = ({
<VideoPlayer
className="absolute mx-auto h-[90vh] w-full max-w-fit rounded-md object-contain"
coverImage={getStorageUrl({
storageId: video.thumb,
id: video.thumb,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
},
})}
videoSrc={getStorageUrl({
storageId: video.video,
id: video.video,
params: {
cacheKey: updated_at?.toISOString(),
media: "video",

View file

@ -309,7 +309,7 @@ function FileList({
>
<div className="col-span-6 flex items-center gap-2 md:col-span-8">
<div className="shrink-0">
<ExtIcon mimeType={file.mimeType} />
<ExtIcon mimeType={file.mime_type} />
</div>
<Link
aria-label="Allegato"
@ -317,17 +317,17 @@ function FileList({
href={`/area-riservata/allegato-view/${file.id}`}
target="_blank"
>
{file.originalName}
{file.filename}
</Link>
</div>
<div className="col-span-4 text-muted-foreground md:col-span-3">
{format(new Date(file.uploadedAt), "d MMM yyyy")}
{format(new Date(file.created_at), "d MMM yyyy")}
</div>
<div className="col-span-2 flex justify-end gap-1 md:col-span-1">
<Link
href={getStorageUrl({
storageId: file.id,
id: file.id,
params: { mode: "download" },
})}
>
@ -352,7 +352,7 @@ function FileList({
<DropdownMenuItem
aria-label="Rinomina"
onClick={() =>
selectHandler(file.id, file.originalName || "")
selectHandler(file.id, file.filename || "")
}
>
<Pencil className="mr-2 size-4" />

View file

@ -6,6 +6,7 @@ import toast from "react-hot-toast";
import { getStorageUrl } from "~/lib/storage_utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useSession } from "~/providers/SessionProvider";
import type { StorageId } from "~/schemas/public/Storage";
import type { UsersId } from "~/schemas/public/Users";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import type { CompleteUserData } from "~/server/services/user.service";
@ -133,7 +134,7 @@ export const DocumentiUploadSection = ({
}: {
title: string;
document: {
storageId: string;
storage_id: StorageId | null;
ref: UsersStorageUserStorageId | null;
} | null;
onUpload: (id: UsersStorageUserStorageId) => void;
@ -145,7 +146,7 @@ export const DocumentiUploadSection = ({
}) => {
const { locale } = useTranslation();
const [imageError, setImageError] = useState(false);
if (document?.storageId) {
if (document?.storage_id) {
return (
<div className="flex flex-col gap-2">
<h4 className="font-medium">{title}</h4>
@ -167,7 +168,7 @@ export const DocumentiUploadSection = ({
height={96}
onError={() => setImageError(true)}
src={getStorageUrl({
storageId: document.storageId,
id: document.storage_id,
})}
width={96}
/>
@ -175,7 +176,7 @@ export const DocumentiUploadSection = ({
<div className="flex size-24 items-center justify-center overflow-clip rounded-md border bg-muted">
<iframe
className="size-full"
src={getStorageUrl({ storageId: document.storageId })}
src={getStorageUrl({ id: document.storage_id })}
title="doc"
>
<p>
@ -188,7 +189,7 @@ export const DocumentiUploadSection = ({
)}
<div className="flex flex-col justify-center gap-2">
<Link
href={`/area-riservata/allegato-view/${document.storageId}`}
href={`/area-riservata/allegato-view/${document.storage_id}`}
target="_blank"
>
<Button size="sm" variant="outline">
@ -231,8 +232,8 @@ export const DocumentiUploadSection = ({
<div className="flex flex-col gap-2">
<h4 className="font-medium">{title}</h4>
<UploadModal
cb_onUpload={({ userStorageIds }) => {
const id = userStorageIds[0];
cb_onUpload={(uploaded) => {
const id = uploaded[0]?.userStorageId;
if (id) onUpload(id);
}}
isAdmin={isAdmin}

View file

@ -38,7 +38,7 @@ export const ChatAttachments = ({
>
<div className="flex grow basis-full items-center gap-2 md:col-span-8">
<div className="shrink-0">
<ExtIcon mimeType={file.mimeType} />
<ExtIcon mimeType={file.mime_type} />
</div>
<Link
aria-label="Allegato"
@ -46,14 +46,14 @@ export const ChatAttachments = ({
href={`/area-riservata/allegato-view/${file.id}`}
target="_blank"
>
{file.originalName}
{file.filename}
</Link>
</div>
<div className="flex justify-end gap-1 md:col-span-1">
<Link
href={getStorageUrl({
storageId: file.id,
id: file.id,
params: { mode: "download" },
})}
>

View file

@ -35,7 +35,6 @@ import { uploadHandler } from "~/hooks/storageClienSideHooks";
import { useMediaQuery } from "~/hooks/use-media-query";
import { useChatContext } from "~/providers/ChatProvider";
import { useTranslation } from "~/providers/I18nProvider";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api";
export default function ChatBottombar() {
@ -68,56 +67,32 @@ export default function ChatBottombar() {
},
});
const utils = api.useUtils();
const { mutateAsync: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onMutate: () => {
toast(t.allegati.allegato_caricato, {
icon: "📤",
toasterId: "uploading-toast",
});
},
onSettled: async () => {
await utils.storage.getAll.invalidate();
await utils.storage.retrieveUserFileData.invalidate({
userId: userinfo.id,
});
},
});
const uploadFiles = async () => {
if (files.length === 0) {
return [];
}
const uploadedFiles: {
storageId: string;
userStorageId: UsersStorageUserStorageId;
}[] = [];
toast(t.allegati.allegato_caricato, {
icon: "📤",
toasterId: "uploading-toast",
});
try {
const from_admin = session.isAdmin;
const { status, attachments } = await uploadHandler({
const { attachments, failed } = await uploadHandler({
files,
from_admin,
});
if (!status) {
toast.error(t.allegati.errore_caricamento, {
for (const file of failed) {
toast.error(`Errore caricamento: ${file}`, {
icon: "❌",
toasterId: "uploading-toast",
});
return [];
}
for (const storageId of attachments) {
const res = await setUserStorage({
storageId,
userId: userinfo.id,
from_admin,
});
if (!res) continue;
uploadedFiles.push({ storageId, userStorageId: res.user_storage_id });
}
return uploadedFiles;
return attachments;
} catch (error) {
toast.error(
error instanceof Error ? error.message : t.allegati.errore_sconosciuto,
@ -125,7 +100,16 @@ export default function ChatBottombar() {
);
return [];
} finally {
toast.success(t.allegati.success_caricamento, {
icon: "✅",
toasterId: "uploading-toast",
});
setFiles([]);
await utils.storage.getAll.invalidate();
await utils.storage.retrieveUserFileData.invalidate({
userId: userinfo.id,
});
}
};

View file

@ -110,7 +110,7 @@ const ContrattoSection = () => {
});
if (selectedDoc) {
setUserStorage({
storageId: selectedDoc,
storage_id: selectedDoc,
userId,
});
}
@ -156,9 +156,9 @@ const ContrattoSection = () => {
}}
/>
<UploadModal
cb_onUpload={({ storageIds }) => {
if (storageIds.length === 1 && storageIds[0]) {
setSelectedDoc(storageIds[0]);
cb_onUpload={(uploaded) => {
if (uploaded.length === 1 && uploaded[0]) {
setSelectedDoc(uploaded[0].storageId);
}
}}
isAdmin
@ -499,7 +499,7 @@ const RicevutaSection = () => {
});
if (selectedDoc) {
setUserStorage({
storageId: selectedDoc,
storage_id: selectedDoc,
userId,
});
}
@ -545,9 +545,9 @@ const RicevutaSection = () => {
}}
/>
<UploadModal
cb_onUpload={({ storageIds }) => {
if (storageIds.length === 1 && storageIds[0]) {
setSelectedDoc(storageIds[0]);
cb_onUpload={(uploaded) => {
if (uploaded.length === 1 && uploaded[0]) {
setSelectedDoc(uploaded[0].storageId);
}
}}
isAdmin

View file

@ -329,7 +329,7 @@ export const AnnuncioDisplay = ({
src={
(data?.images[0] &&
getStorageUrl({
storageId: data.images[0].img,
id: data.images[0].img,
params: {
cacheKey: data.media_updated_at?.toISOString(),
media: "image",

View file

@ -45,9 +45,9 @@ export const DocInfo = ({
target="_blank"
>
<Button className="w-fit grow" variant="outline">
<ExtIcon mimeType={file_info.mimeType} />
<ExtIcon mimeType={file_info.mime_type} />
<span className="max-w-40 truncate sm:max-w-80">
{file_info.originalName}
{file_info.filename}
</span>
<ExternalLink className="size-4" />
</Button>
@ -103,10 +103,8 @@ export const DocCombo = ({
value={file.id}
>
<span className="flex items-center gap-2">
<ExtIcon mimeType={file.mimeType} />
<span className="block font-medium">
{file.originalName}
</span>
<ExtIcon mimeType={file.mime_type} />
<span className="block font-medium">{file.filename}</span>
{current === file.id && (
<Check className="size-4 text-accent-foreground" />
)}

View file

@ -1,4 +1,4 @@
import type { ColumnDef, Table } from "@tanstack/react-table";
import type { ColumnDef } from "@tanstack/react-table";
import { ExternalLink, MoreHorizontal, Pen, Trash } from "lucide-react";
import Link from "next/link";
import { type FormEvent, useEffect, useState } from "react";
@ -36,7 +36,10 @@ import {
} from "~/components/ui/dropdown-menu";
import Input from "~/components/ui/input";
import { cn } from "~/lib/utils";
import { useStorageTable } from "~/providers/StorageTableProvider";
import {
type StorageTableType,
useStorageTable,
} from "~/providers/StorageTableProvider";
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
import { api } from "~/utils/api";
@ -56,8 +59,18 @@ export const StorageTable = ({
allegatoId: string;
filename: string | null;
} | null>(null);
const tabledata = data;
const [tableInstance, setTableInstance] = useState<Table<Column> | null>(
const columns_titles = {
actions: "Azioni",
created_at: "Data creazione",
mime_type: "Tipo file",
filename: "Nome file",
from_admin: "Caricato da",
id: "Codice",
};
type Column = (typeof tabledata)[number];
const [tableInstance, setTableInstance] = useState<StorageTableType | null>(
null,
);
@ -68,18 +81,6 @@ export const StorageTable = ({
}
}, [tableInstance, setTable]);
const tabledata = data;
const columns_titles = {
actions: "Azioni",
uploadedAt: "Data creazione",
type: "Tipo file",
originalName: "Nome file",
from_admin: "Caricato da",
id: "Codice",
};
type Column = (typeof tabledata)[number];
const columns: ColumnDef<Column>[] = [
{
cell: ({ row }) => (
@ -104,7 +105,7 @@ export const StorageTable = ({
id: "select",
},
{
accessorKey: "originalName",
accessorKey: "filename",
cell: ({ row }) => {
return (
<Link
@ -113,7 +114,7 @@ export const StorageTable = ({
href={`/area-riservata/allegato-view/${row.original.id}`}
target="_blank"
>
{row.original.originalName}
{row.original.filename}
</Link>
);
},
@ -121,14 +122,14 @@ export const StorageTable = ({
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.originalName}
title={columns_titles.filename}
/>
),
},
{
accessorKey: "uploadedAt",
accessorKey: "created_at",
cell: ({ row }) => {
const date = row.getValue("uploadedAt");
const date = row.getValue("created_at");
return new Date(date as Date).toLocaleDateString();
},
@ -136,7 +137,7 @@ export const StorageTable = ({
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.uploadedAt}
title={columns_titles.created_at}
/>
),
},
@ -187,7 +188,7 @@ export const StorageTable = ({
onClick={() =>
setRenameTarget({
allegatoId: data.id,
filename: data.originalName,
filename: data.filename,
})
}
role="button"
@ -215,7 +216,7 @@ export const StorageTable = ({
];
const searchFiltro: SearchFiltro = {
columnName: "originalName",
columnName: "filename",
placeholder: "Cerca file ...",
};
return (
@ -224,7 +225,7 @@ export const StorageTable = ({
columns={columns}
columns_titles={columns_titles}
data={tabledata}
defaultSort={[{ desc: true, id: "uploadedAt" }]}
defaultSort={[{ desc: true, id: "created_at" }]}
onStateChange={cb_onStateChange}
onTableInit={setTableInstance}
pinnedFiltri={undefined}

View file

@ -22,16 +22,12 @@ import {
FileUploadTrigger,
} from "~/components/custom_ui/fileUpload";
import { Button } from "~/components/ui/button";
import { uploadHandler } from "~/hooks/storageClienSideHooks";
import { type Attachment, uploadHandler } from "~/hooks/storageClienSideHooks";
import { useTranslation } from "~/providers/I18nProvider";
import type { UsersId } from "~/schemas/public/Users";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api";
type UploadCallback = (args: {
storageIds: string[];
userStorageIds: UsersStorageUserStorageId[];
}) => void;
type UploadCallback = (uploaded: Attachment[]) => void;
export const UploadModal = (props: UploadComponentProps) => {
const { t } = useTranslation();
@ -78,22 +74,6 @@ const UploadComponent = ({
const [files, setFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
const { mutateAsync: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onMutate: () => {
toast(t.allegati.allegato_caricato, {
icon: "📤",
toasterId: "uploading-toast",
});
},
onSettled: async () => {
await utils.storage.getAll.invalidate();
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
},
});
const acceptedMimeTypes = [
"image/jpeg", // jpg, jpeg
"image/png", // png
@ -161,37 +141,29 @@ const UploadComponent = ({
if (files.length === 0) {
return;
}
let storageIds: string[] = [];
const userStorageIds: UsersStorageUserStorageId[] = [];
toast(t.allegati.allegato_caricato, {
icon: "📤",
toasterId: "uploading-toast",
});
let uploaded: Attachment[] = [];
try {
const from_admin = isAdmin ?? true;
setIsUploading(true);
const { status, attachments } = await uploadHandler({
const { attachments, failed } = await uploadHandler({
files,
from_admin,
});
if (!status) {
toast.error(t.allegati.errore_caricamento, {
for (const file of failed) {
toast.error(`Errore caricamento: ${file}`, {
icon: "❌",
toasterId: "uploading-toast",
});
setIsUploading(false);
return;
}
storageIds = attachments;
if (userId) {
for (const storageId of attachments) {
const res = await setUserStorage({
storageId,
userId,
from_admin,
});
if (!res) continue;
userStorageIds.push(res.user_storage_id);
}
}
uploaded = attachments;
files.forEach((file) => {
const randomDelay = Math.random() * 1000 + 500; // Random delay between 500ms and 1500ms
@ -206,11 +178,6 @@ const UploadComponent = ({
onProgress(file, progress);
}, randomDelay / 1); // Adjust interval based on random delay
});
toast.success(t.allegati.success_caricamento, {
icon: "✅",
toasterId: "uploading-toast",
});
} catch (error) {
setIsUploading(false);
@ -221,10 +188,19 @@ const UploadComponent = ({
{ icon: "❌", toasterId: "uploading-toast" },
);
} finally {
toast.success(t.allegati.success_caricamento, {
icon: "✅",
toasterId: "uploading-toast",
});
setIsUploading(false);
setFiles([]);
cb_onUpload?.({ storageIds, userStorageIds });
cb_onUpload?.(uploaded);
await utils.storage.getAll.invalidate();
if (userId) {
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
}
}
},
[],

View file

@ -25,8 +25,6 @@ export const env = createEnv({
SKEBBY_USER: z.string(),
SKEBBY_PASS: z.string(),
REVALIDATION_SECRET: z.string(),
STORAGE_URL: z.string(),
STORAGE_TOKEN: z.string(),
},
client: {
NEXT_PUBLIC_BASE_URL: z.string(),

View file

@ -1875,7 +1875,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => {
height={400}
key={img.img}
src={getStorageUrl({
storageId: img.img,
id: img.img,
params: {
cacheKey: data.media_updated_at?.toISOString(),
media: "image",
@ -1906,7 +1906,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => {
<VideoPlayer
className="aspect-square size-52 object-cover"
coverImage={getStorageUrl({
storageId: video.thumb,
id: video.thumb,
params: {
cacheKey: data.media_updated_at?.toISOString(),
media: "image",
@ -1914,7 +1914,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => {
})}
key={`videoplayer-${video.video}`}
videoSrc={getStorageUrl({
storageId: video.video,
id: video.video,
params: {
cacheKey: data.media_updated_at?.toISOString(),
media: "video",

View file

@ -1,50 +0,0 @@
"use client";
import { deleteCookie } from "cookies-next";
import { useEffect } from "react";
const STALE_KEY = "stale-resource";
const RELOAD_STORAGE_KEY = "stale-img-reload";
const RELOAD_COOLDOWN_MS = 10 * 1000;
export const useStaleImageReload = () => {
useEffect(() => {
// Check for stale resource cookie
const checkStaleCookie = () => {
const hasStale = document.cookie.includes(`${STALE_KEY}=1`);
if (hasStale) {
//console.log("Stale resource cookie detected");
// Clear the cookie
deleteCookie(STALE_KEY);
attemptReload();
}
};
// Check on mount
checkStaleCookie();
// Also check periodically for dynamically loaded images
const interval = setInterval(checkStaleCookie, 1000);
return () => clearInterval(interval);
}, []);
};
function attemptReload() {
const lastAttempt = sessionStorage.getItem(RELOAD_STORAGE_KEY);
const now = Date.now();
if (!lastAttempt) {
sessionStorage.setItem(RELOAD_STORAGE_KEY, now.toString());
window.location.reload();
return;
}
const timeSinceLastAttempt = now - parseInt(lastAttempt, 10);
if (timeSinceLastAttempt > RELOAD_COOLDOWN_MS) {
sessionStorage.setItem(RELOAD_STORAGE_KEY, now.toString());
window.location.reload();
}
}

View file

@ -1,11 +1,19 @@
import { add } from "date-fns";
import z from "zod";
import { storageId } from "~/schemas/public/Storage";
import type { UsersId } from "~/schemas/public/Users";
import { usersStorageUserStorageId } from "~/schemas/public/UsersStorage";
const attachmentSchema = z.object({
storageId: storageId,
userStorageId: usersStorageUserStorageId.nullable(),
});
export type Attachment = z.infer<typeof attachmentSchema>;
type UploadResponse =
| {
| (Attachment & {
success: true;
id: string;
}
})
| {
success: false;
error: string;
@ -14,6 +22,7 @@ type UploadResponse =
async function uploadFile(
file: File,
expiresAt: string | undefined,
userId?: UsersId,
): Promise<UploadResponse> {
try {
const formData = new FormData();
@ -21,27 +30,25 @@ async function uploadFile(
if (expiresAt) {
formData.append("expires_at", expiresAt);
}
if (userId) {
formData.append("userId", userId);
}
formData.append("bucket", "storage");
const response = await fetch(`/storage-api/upload`, {
const response = await fetch(`/api/storage/upload`, {
method: "POST",
body: formData,
});
if (response.ok) {
const res = await response.json();
const parsed = z
.object({
id: z.string(),
})
.parse(res);
const fileID = parsed.id;
return { success: true, id: fileID };
const parsed = attachmentSchema.parse(res);
return { success: true, ...parsed };
}
const res = (await response.json()) as { message: string };
return {
success: false,
error: `Caricamento fallito con status: ${response.status}`,
error: `Caricamento fallito con status: ${response.status}, ${res.message}`,
};
} catch (e) {
return {
@ -54,30 +61,33 @@ async function uploadFile(
export const uploadHandler = async ({
files,
from_admin,
userId,
}: {
files: File[];
from_admin: boolean;
userId?: UsersId;
}) => {
const attachments = [];
const attachments: Attachment[] = [];
const failed: string[] = [];
const expirationDate = add(new Date(), { days: 60 }).toISOString();
if (files.length === 0) {
console.error("No files provided");
return { attachments: [], error: "No files provided", status: false };
}
for (const file of files) {
const upload = await uploadFile(
file,
from_admin ? expirationDate : undefined,
userId,
);
if (upload.success) {
attachments.push(upload.id);
attachments.push({
storageId: upload.storageId,
userStorageId: upload.userStorageId,
});
} else {
console.error(`Error uploading file ${file.name}: ${upload.error}`);
failed.push(file.name);
}
}
return { attachments, failed, status: true };
return { attachments, failed };
};

View file

@ -1,21 +1,22 @@
import { env } from "~/env";
import type { StorageId } from "~/schemas/public/Storage";
type Params = {
type StorageParams = {
cacheKey: string;
mode: "download" | "inline";
media: "image" | "video";
};
export function getStorageUrl({
storageId,
id,
params,
host = env.NEXT_PUBLIC_BASE_URL,
}: {
storageId: string;
params?: Partial<Params>;
id: StorageId;
params?: Partial<StorageParams>;
host?: string;
}) {
const url = new URL(`${host}/storage-api/get/${storageId}`);
const url = new URL(`${host}/api/storage/get/${id}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);

View file

@ -327,9 +327,10 @@ const AnnunciList = () => {
maxBudget: maxBudget || undefined,
},
{
gcTime: 60 * 1000,
enabled: !map && !isUpdatingRicerca,
placeholderData: keepPreviousData,
staleTime: 5000,
staleTime: 60 * 1000,
},
);

View file

@ -55,7 +55,6 @@ import { Label } from "~/components/ui/label";
import { VideoPlayer } from "~/components/videoPlayer";
import { env } from "~/env";
import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils";
import { useStaleImageReload } from "~/hooks/staleImage";
import { useMediaQuery } from "~/hooks/use-media-query";
import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr";
@ -120,8 +119,6 @@ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
};
const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
useStaleImageReload();
const isDesktop = useMediaQuery("(min-width: 768px)");
if (!data || (data && data.stato === "Sospeso")) {
@ -662,7 +659,7 @@ const AnnuncioFooter = () => {
<VideoPlayer
className="h-96 max-w-96"
coverImage={getStorageUrl({
storageId: video.thumb,
id: video.thumb,
params: {
cacheKey: media_updated_at || undefined,
media: "image",
@ -670,7 +667,7 @@ const AnnuncioFooter = () => {
})}
key={`videoplayer-${video.video}`} // Cache busting
videoSrc={getStorageUrl({
storageId: video.video,
id: video.video,
params: {
cacheKey: media_updated_at || undefined,
media: "video",

View file

@ -0,0 +1,144 @@
import fs from "node:fs";
import path from "node:path";
import type { NextApiRequest, NextApiResponse } from "next";
import { storageId } from "~/schemas/public/Storage";
import { TOKEN_CONFIG } from "~/server/auth/configs";
import { verifyToken } from "~/server/auth/jwt";
import { getFile } from "~/server/services/storage.service";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "GET") {
res.setHeader("Allow", ["GET"]);
return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
}
const { id, cacheKey, mode, media } = req.query;
if (!id || Array.isArray(id)) {
return res.status(400).json({ error: "Invalid or missing file ID" });
}
const referer = req.headers.referer;
const purpose = req.headers.purpose;
const isNextImageRequest =
purpose === "prefetch" || req.headers["x-vercel-id"];
const isImageRequest = media === "image";
const isVideoRequest = media === "video";
const isDownloadRequest = mode === "download";
const cookies = req.cookies;
const eTag = `W/"${cacheKey}"`;
const ifNoneMatch = req.headers["if-none-match"];
const ifModifiedSince = req.headers["if-modified-since"];
if (isImageRequest || isNextImageRequest || isVideoRequest) {
if (ifNoneMatch === eTag || ifModifiedSince === cacheKey) {
// 🎉 Cache HIT! Tell the browser to reuse its local copy.
// We send NO body payload here, saving massive server bandwidth and CPU.
res.status(304).end();
return;
}
}
try {
if (!isNextImageRequest && !isImageRequest && !isVideoRequest) {
const accessToken = cookies[TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME];
if (!accessToken) {
console.log(
"No access token found, referrer:",
referer,
" url:",
req.url,
);
return res.status(401);
}
const { expired, session } = await verifyToken(accessToken);
if (!session || expired) {
console.log("Invalid token, referrer:", referer, " url:", req.url);
return res.status(401);
}
}
const parsedId = storageId.safeParse(id);
if (!parsedId.success) {
if (isImageRequest || isNextImageRequest) {
return getFallback("fallback-image.png", req, res);
}
if (isVideoRequest) {
return getFallback("fallback-video.png", req, res);
}
return res.status(404).json({ error: "Invalid input id" });
}
const file = await getFile(parsedId.data);
if (!file) {
if (isImageRequest || isNextImageRequest) {
return getFallback("fallback-image.png", req, res);
}
if (isVideoRequest) {
return getFallback("fallback-video.png", req, res);
}
return res.status(404).json({ error: "File not found" });
}
res.setHeader("Content-Type", file.mime_type);
res.setHeader("Content-Length", file.file_size_bytes);
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("ETag", eTag);
res.setHeader("Last-Modified", String(cacheKey));
res.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
if (isDownloadRequest) {
res.setHeader(
"Content-Disposition",
`attachment; filename="${file.filename}"`,
);
} else {
res.setHeader(
"Content-Disposition",
`inline; filename="${file.filename}"`,
);
}
return res.status(200).send(file.file_data);
} catch (error) {
console.error("Pages Router Media API Error:", error);
if (isImageRequest || isNextImageRequest) {
return getFallback("fallback-image.png", req, res);
}
if (isVideoRequest) {
return getFallback("fallback-video.png", req, res);
}
return res.status(500).json({ error: "Internal server error" });
}
}
async function getFallback(
img: string,
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.headers["if-none-match"] === '"fallback-v1"') {
res.status(304).end(); // Instantly reuse client-side fallback
return;
}
try {
const fallbackPath = path.join(process.cwd(), "public", img);
const imageBuffer = fs.readFileSync(fallbackPath);
res.setHeader("Content-Type", "image/png");
res.setHeader(
"Cache-Control",
"public, max-age=0, no-cache, must-revalidate",
);
res.setHeader("Content-Length", imageBuffer.length);
res.setHeader("ETag", '"fallback-v1"');
return res.status(200).send(imageBuffer);
} catch (e) {
console.error(e);
return res
.status(404)
.json({ error: "File and fallback placeholder not found" });
}
}
export const config = {
api: {
responseLimit: false,
},
};

View file

@ -0,0 +1,80 @@
import fs from "node:fs";
import { IncomingForm } from "formidable";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { usersId } from "~/schemas/public/Users";
import { uploadFile } from "~/server/services/storage.service";
const schema = z.object({
userId: usersId.optional(),
expires_at: z.date().optional(),
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const form = new IncomingForm();
form.parse(req, async (err, fields, files) => {
if (err) {
return res.status(500).json({ message: "Form parsing failed" });
}
// Extract the file safely from formidable payload
const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file;
if (!uploadedFile) {
return res.status(400).json({ message: "No file found in payload" });
}
try {
// Read raw uncorrupted bytes straight from temporary disk space
const fileBuffer = fs.readFileSync(uploadedFile.filepath);
// Clean up optional fields and map variables correctly
const expiresAtRaw = Array.isArray(fields.expires_at)
? fields.expires_at[0]
: fields.expires_at;
const userIdRaw = Array.isArray(fields.userId)
? fields.userId[0]
: fields.userId;
const meta = schema.safeParse({
userId: userIdRaw,
expires_at: expiresAtRaw ? new Date(expiresAtRaw) : undefined,
});
if (meta.success) {
// Call database insertion code
const { storageId, userStorageId } = await uploadFile({
buffer: fileBuffer,
filename: uploadedFile.originalFilename || "unknown_file",
mime_type: uploadedFile.mimetype || "application/octet-stream",
expires_at: meta.data.expires_at,
userId: meta.data.userId,
});
// Delete local temporary file to free host disk space
fs.unlinkSync(uploadedFile.filepath);
return res.status(200).json({ storageId, userStorageId });
}
fs.unlinkSync(uploadedFile.filepath);
return res.status(500).json({ message: meta.error.message });
} catch (error) {
console.error(error);
return res.status(500).json({ message: (error as Error).message });
}
});
}
export const config = {
api: {
responseLimit: false,
bodyParser: false,
},
};

View file

@ -36,7 +36,7 @@ import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import {
StorageTableProvider,
type StorageTable as StorageTableType,
type StorageTableType,
useStorageTable,
} from "~/providers/StorageTableProvider";
@ -286,7 +286,7 @@ const VisibComponent = ({
onSelect={(user) => {
for (const allegato of storageIds) {
add({
storageId: allegato,
storage_id: allegato,
userId: user.id,
from_admin: true,
});
@ -311,7 +311,7 @@ const VisibComponent = ({
key={allegato.id}
>
<p className="font-semibold text-lg">
File: {allegato.originalName}
File: {allegato.filename}
</p>
<div className="flex items-center gap-3">
<p>Visibile a:</p>

View file

@ -72,19 +72,19 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
return (
<>
<Head>
<title>Infoalloggi.it | {fileInfos.originalName}</title>
<title>Infoalloggi.it | {fileInfos.filename}</title>
</Head>
<div className="flex h-full w-full grow flex-col md:flex-row">
<div className="flex grow flex-col justify-center space-y-2 p-4">
<div className="mx-auto flex flex-wrap items-center gap-4">
<span>{fileInfos.originalName}</span>
<span>{fileInfos.filename}</span>
<span>
caricato il {new Date(fileInfos.uploadedAt).toLocaleString("it")}
caricato il {new Date(fileInfos.created_at).toLocaleString("it")}
</span>
<Link
href={getStorageUrl({
storageId: fileInfos.id,
id: fileInfos.id,
params: { mode: "download" },
})}
>
@ -95,7 +95,7 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
</Link>
<RinominaDialog
allegatoId={allegatoId}
filename={fileInfos.originalName}
filename={fileInfos.filename}
/>
{session.user.isAdmin && (
<Confirm

View file

@ -45,7 +45,7 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
<Link
className="underline underline-offset-1 after:content-['_↗']"
href={getStorageUrl({
storageId: fileInfos.id,
id: fileInfos.id,
params: { mode: "download" },
})}
>

View file

@ -41,7 +41,7 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
<Link
className="underline underline-offset-1 after:content-['_↗']"
href={getStorageUrl({
storageId: fileInfos.id,
id: fileInfos.id,
params: { mode: "download" },
})}
>

View file

@ -10,11 +10,11 @@ import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
type TData = FileMetadataWithAdmin;
export type StorageTable = Table<TData>;
export type StorageTableType = Table<TData>;
const StorageTableContext = createContext<{
table: StorageTable | undefined;
setTable: (table: StorageTable | undefined) => void;
table: StorageTableType | undefined;
setTable: (table: StorageTableType | undefined) => void;
selectedRows: TData[];
cb_onStateChange: () => void;
}>({
@ -26,7 +26,7 @@ const StorageTableContext = createContext<{
});
export const StorageTableProvider = ({ children }: { children: ReactNode }) => {
const [table, setTable] = useState<StorageTable | undefined>(undefined);
const [table, setTable] = useState<StorageTableType | undefined>(undefined);
const [selectedRows, setSelectedRows] = useState<TData[]>([]);

View file

@ -1,216 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { env } from "~/env";
import type { ProxyFn } from "~/proxy";
import { TOKEN_CONFIG } from "~/server/auth/configs";
import { verifyToken } from "~/server/auth/jwt";
const STALE_KEY = "stale-resource";
export const apisProxy: ProxyFn = async (req: NextRequest) => {
const { pathname, searchParams } = req.nextUrl;
// Handle Storage API requests
if (pathname.startsWith("/storage-api/")) {
const params = new URLSearchParams(searchParams);
const isImageRequest = params.get("media") === "image";
const isVideoRequest = params.get("media") === "video";
const purpose = req.headers.get("purpose");
const isNextImageRequest =
purpose === "prefetch" || req.headers.get("x-vercel-id");
if (!isNextImageRequest && !isImageRequest && !isVideoRequest) {
const accessToken = req.cookies.get(
TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME,
)?.value;
if (!accessToken) {
console.log(
"No access token found, referrer:",
req.referrer,
" url:",
req.url,
);
return new NextResponse("Unauthorized", { status: 401 });
}
const { expired, session } = await verifyToken(accessToken);
if (!session || expired) {
console.log("Invalid token, referrer:", req.referrer, " url:", req.url);
return new NextResponse("Unauthorized", { status: 401 });
}
}
const slug = pathname.replace("/storage-api/", "");
params.set("token", env.STORAGE_TOKEN);
const storageUrl = `${env.STORAGE_URL}/${slug}?${params.toString()}`;
try {
const headers = new Headers(req.headers);
headers.delete("host");
headers.delete("cookie");
const rangeHeader = req.headers.get("range");
const response = await fetch(storageUrl, {
method: req.method,
headers: headers,
body:
req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
// @ts-expect-error - duplex needed for request streaming
duplex: "half",
});
if (!response.ok) {
const isStaleResource =
response.status === 404 || response.status === 410;
if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png";
url.search = "";
const response = NextResponse.rewrite(url);
if (isStaleResource) {
response.cookies.set(STALE_KEY, "1", {
maxAge: 5,
path: "/",
});
}
return response;
}
if (isVideoRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png";
url.search = "";
const response = NextResponse.rewrite(url);
if (isStaleResource) {
response.cookies.set(STALE_KEY, "1", {
maxAge: 5,
path: "/",
});
}
return response;
}
if (isStaleResource) {
return NextResponse.json(
{ message: "Stale resource, please reload." },
{ status: 410 },
);
}
return new NextResponse(`Storage API error: ${response.status}`, {
status: response.status,
});
}
if (response.status === 204) {
return new NextResponse(null, { status: 200 });
}
const data = await response.arrayBuffer();
if (!data || data.byteLength === 0) {
console.error(
"Received empty response from storage API, referrer:",
req.referrer,
" url:",
req.url,
);
if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png";
url.search = "";
return NextResponse.rewrite(url);
}
if (isVideoRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png";
url.search = "";
return NextResponse.rewrite(url);
}
return new NextResponse("Empty response", { status: 500 });
}
const status =
rangeHeader && response.status === 206 ? 206 : response.status;
const proxyResponse = new NextResponse(data, {
status: status,
statusText: response.statusText,
});
const contentType = response.headers.get("Content-Type");
const contentDisposition = response.headers.get("Content-Disposition");
const contentLength = response.headers.get("Content-Length");
const acceptRanges = response.headers.get("Accept-Ranges");
const contentRange = response.headers.get("Content-Range");
if (contentType) {
proxyResponse.headers.set("Content-Type", contentType);
}
if (contentDisposition) {
proxyResponse.headers.set("Content-Disposition", contentDisposition);
}
if (contentLength) {
proxyResponse.headers.set("Content-Length", contentLength);
}
if (acceptRanges) {
proxyResponse.headers.set("Accept-Ranges", acceptRanges);
} else if (isVideoRequest) {
proxyResponse.headers.set("Accept-Ranges", "bytes");
}
if (contentRange) {
proxyResponse.headers.set("Content-Range", contentRange);
}
proxyResponse.headers.set("Access-Control-Allow-Origin", "*");
if (isVideoRequest) {
proxyResponse.headers.set(
"Cache-Control",
"public, max-age=31536000, immutable",
);
} else {
proxyResponse.headers.set(
"Cache-Control",
"public, max-age=31536000, immutable",
);
}
proxyResponse.headers.delete("X-Frame-Options");
proxyResponse.headers.set(
"Content-Security-Policy",
"frame-ancestors 'self'",
);
return proxyResponse;
} catch (error) {
console.error("Storage proxy error:", error);
if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png";
url.search = "";
return NextResponse.rewrite(url);
}
if (isVideoRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png";
url.search = "";
return NextResponse.rewrite(url);
}
return new NextResponse("Failed to proxy request", { status: 500 });
}
}
return;
};

View file

@ -1,6 +1,5 @@
import { type NextRequest, NextResponse } from "next/server";
import { authProxy } from "~/proxies/auth";
import { apisProxy } from "./proxies/api";
import { pswChangeProxy } from "./proxies/psw_change";
import { TestRedirectProxy } from "./proxies/test_redirect";
@ -10,7 +9,6 @@ export async function proxy(request: NextRequest) {
return await chainProxies(request, [
TestRedirectProxy,
//updaterProxy,
apisProxy,
pswChangeProxy,
authProxy,
]);
@ -18,7 +16,6 @@ export async function proxy(request: NextRequest) {
export const config = {
matcher: [
"/storage-api/:path*",
{
source: "/",
},

View file

@ -1,3 +1,4 @@
import { storageId, type StorageId } from './Storage';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
import { z } from 'zod';
@ -7,9 +8,9 @@ export default interface ImagesRefsTable {
ordine: ColumnType<number, number, number>;
img: ColumnType<string, string, string>;
img: ColumnType<StorageId, StorageId, StorageId>;
thumb: ColumnType<string, string, string>;
thumb: ColumnType<StorageId, StorageId, StorageId>;
og_url: ColumnType<string, string, string>;
}
@ -23,23 +24,23 @@ export type ImagesRefsUpdate = Updateable<ImagesRefsTable>;
export const ImagesRefsSchema = z.object({
codice: z.string(),
ordine: z.number(),
img: z.string(),
thumb: z.string(),
img: storageId,
thumb: storageId,
og_url: z.string(),
});
export const NewImagesRefsSchema = z.object({
codice: z.string(),
ordine: z.number(),
img: z.string(),
thumb: z.string(),
img: storageId,
thumb: storageId,
og_url: z.string(),
});
export const ImagesRefsUpdateSchema = z.object({
codice: z.string().optional(),
ordine: z.number().optional(),
img: z.string().optional(),
thumb: z.string().optional(),
img: storageId.optional(),
thumb: storageId.optional(),
og_url: z.string().optional(),
});

View file

@ -26,6 +26,7 @@ import type { default as ProvincieTable } from './Provincie';
import type { default as VideosRefsTable } from './VideosRefs';
import type { default as BanlistTable } from './Banlist';
import type { default as NazioniTable } from './Nazioni';
import type { default as StorageTable } from './Storage';
import type { default as AppuntiGroupsTable } from './AppuntiGroups';
import type { default as ImagesRefsTable } from './ImagesRefs';
import type { default as BannersTable } from './Banners';
@ -89,6 +90,8 @@ export default interface PublicSchema {
nazioni: NazioniTable;
storage: StorageTable;
appunti_groups: AppuntiGroupsTable;
images_refs: ImagesRefsTable;

View file

@ -0,0 +1,65 @@
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
import { z } from 'zod';
/** Identifier type for public.storage */
export type StorageId = string & { __brand: 'public.storage' };
/** Represents the table public.storage */
export default interface StorageTable {
id: ColumnType<StorageId, StorageId | undefined, StorageId>;
created_at: ColumnType<Date, Date | string | undefined, Date | string>;
filename: ColumnType<string, string, string>;
mime_type: ColumnType<string, string, string>;
file_size_bytes: ColumnType<number, number, number>;
file_data: ColumnType<Buffer, Buffer, Buffer>;
expires_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
tag: ColumnType<string | null, string | null, string | null>;
}
export type Storage = Selectable<StorageTable>;
export type NewStorage = Insertable<StorageTable>;
export type StorageUpdate = Updateable<StorageTable>;
export const storageId = z.uuid().transform(value => value as StorageId);
export const StorageSchema = z.object({
id: storageId,
created_at: z.date(),
filename: z.string(),
mime_type: z.string(),
file_size_bytes: z.number(),
file_data: z.instanceof(Buffer),
expires_at: z.date().nullable(),
tag: z.string().nullable(),
});
export const NewStorageSchema = z.object({
id: storageId.optional(),
created_at: z.union([z.date(), z.string()]).pipe(z.coerce.date()).optional(),
filename: z.string(),
mime_type: z.string(),
file_size_bytes: z.number(),
file_data: z.instanceof(Buffer),
expires_at: z.union([z.date(), z.string()]).pipe(z.coerce.date()).optional().nullable(),
tag: z.string().optional().nullable(),
});
export const StorageUpdateSchema = z.object({
id: storageId.optional(),
created_at: z.union([z.date(), z.string()]).pipe(z.coerce.date()).optional(),
filename: z.string().optional(),
mime_type: z.string().optional(),
file_size_bytes: z.number().optional(),
file_data: z.instanceof(Buffer).optional(),
expires_at: z.union([z.date(), z.string()]).pipe(z.coerce.date()).optional().nullable(),
tag: z.string().optional().nullable(),
});

View file

@ -1,4 +1,5 @@
import { usersId, type UsersId } from './Users';
import { storageId, type StorageId } from './Storage';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
import { z } from 'zod';
@ -11,9 +12,9 @@ export default interface UsersStorageTable {
userId: ColumnType<UsersId, UsersId, UsersId>;
storageId: ColumnType<string, string, string>;
from_admin: ColumnType<boolean, boolean | undefined, boolean>;
storage_id: ColumnType<StorageId | null, StorageId | null, StorageId | null>;
}
export type UsersStorage = Selectable<UsersStorageTable>;
@ -27,20 +28,20 @@ export const usersStorageUserStorageId = z.uuid().transform(value => value as Us
export const UsersStorageSchema = z.object({
user_storage_id: usersStorageUserStorageId,
userId: usersId,
storageId: z.string(),
from_admin: z.boolean(),
storage_id: storageId.nullable(),
});
export const NewUsersStorageSchema = z.object({
user_storage_id: usersStorageUserStorageId.optional(),
userId: usersId,
storageId: z.string(),
from_admin: z.boolean().optional(),
storage_id: storageId.optional().nullable(),
});
export const UsersStorageUpdateSchema = z.object({
user_storage_id: usersStorageUserStorageId.optional(),
userId: usersId.optional(),
storageId: z.string().optional(),
from_admin: z.boolean().optional(),
storage_id: storageId.optional().nullable(),
});

View file

@ -1,3 +1,4 @@
import { storageId, type StorageId } from './Storage';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
import { z } from 'zod';
@ -7,9 +8,9 @@ export default interface VideosRefsTable {
ordine: ColumnType<number, number, number>;
video: ColumnType<string, string, string>;
video: ColumnType<StorageId, StorageId, StorageId>;
thumb: ColumnType<string, string, string>;
thumb: ColumnType<StorageId, StorageId, StorageId>;
og_url: ColumnType<string, string, string>;
}
@ -23,23 +24,23 @@ export type VideosRefsUpdate = Updateable<VideosRefsTable>;
export const VideosRefsSchema = z.object({
codice: z.string(),
ordine: z.number(),
video: z.string(),
thumb: z.string(),
video: storageId,
thumb: storageId,
og_url: z.string(),
});
export const NewVideosRefsSchema = z.object({
codice: z.string(),
ordine: z.number(),
video: z.string(),
thumb: z.string(),
video: storageId,
thumb: storageId,
og_url: z.string(),
});
export const VideosRefsUpdateSchema = z.object({
codice: z.string().optional(),
ordine: z.number().optional(),
video: z.string().optional(),
thumb: z.string().optional(),
video: storageId.optional(),
thumb: storageId.optional(),
og_url: z.string().optional(),
});

View file

@ -8,8 +8,12 @@ import {
type MessagesMessageid,
messagesMessageid,
} from "~/schemas/public/Messages";
import { storageId } from "~/schemas/public/Storage";
import { usersId } from "~/schemas/public/Users";
import { usersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import {
type UsersStorageUserStorageId,
usersStorageUserStorageId,
} from "~/schemas/public/UsersStorage";
import {
adminProcedure,
createTRPCRouter,
@ -260,8 +264,8 @@ export const chatSSERouter = createTRPCRouter({
reply_to_id: messagesMessageid.nullable().default(null),
attachments: z
.object({
userStorageId: usersStorageUserStorageId,
storageId: z.string(),
userStorageId: usersStorageUserStorageId.nullable(),
storageId: storageId,
})
.array()
.default([]),
@ -285,15 +289,19 @@ export const chatSSERouter = createTRPCRouter({
);
let hasAttachments = false;
if (input.attachments.length > 0) {
const values = input.attachments
.filter((a) => a.userStorageId !== null)
.map(({ userStorageId }) => ({
messageId: newMessage.messageid,
userStorageId,
})) as {
messageId: MessagesMessageid;
userStorageId: UsersStorageUserStorageId;
}[];
const newAttachments = await db
.$pickTables<"messages_attachments">()
.insertInto("messages_attachments")
.values(
input.attachments.map(({ userStorageId }) => ({
messageId: newMessage.messageid,
userStorageId,
})),
)
.values(values)
.returning("userStorageId")
.execute();
hasAttachments = newAttachments.length > 0;

View file

@ -1,11 +1,12 @@
import { TRPCError } from "@trpc/server";
import z from "zod";
import { storageId } from "~/schemas/public/Storage";
import { usersId } from "~/schemas/public/Users";
import { NewUsersStorageSchema } from "~/schemas/public/UsersStorage";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "~/server/api/trpc";
import {
addUserStorage,
@ -19,36 +20,32 @@ import { db } from "~/server/db";
import {
deleteFile,
editFile,
getFile,
getFileInfo,
} from "~/server/services/storage.service";
export const storageRouter = createTRPCRouter({
getFileInfos: protectedProcedure
.input(z.object({ storageId: z.string() }))
.input(z.object({ storageId: storageId }))
.query(async ({ input }) => {
return (await getFileInfo(input.storageId)) || undefined;
}),
deleteFile: protectedProcedure
.input(z.object({ storageId: z.string() }))
.input(z.object({ storageId: storageId }))
.mutation(async ({ input }) => {
const deletion = await deleteFile(input.storageId);
if (!deletion.success) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: deletion.error,
});
}
await deleteFile(input.storageId);
await purgeFileFromUserStorages({
...input,
});
}),
deleteUserStorageEntry: protectedProcedure
.input(z.object({ storageId: z.string(), userId: usersId }))
.input(z.object({ storageId: storageId, userId: usersId }))
.mutation(async ({ input }) => {
return await deleteUserStorageEntry(input);
}),
deleteFilesFromUserStorage: adminProcedure
.input(z.object({ storageIds: z.string().array() }))
.input(z.object({ storageIds: storageId.array() }))
.mutation(async ({ input }) => {
for (const storageId of input.storageIds) {
await purgeFileFromUserStorages({ storageId });
@ -61,7 +58,7 @@ export const storageRouter = createTRPCRouter({
return await retrieveUserFiles(input);
}),
getStorageAccessDetails: adminProcedure
.input(z.object({ storageIds: z.string().array() }))
.input(z.object({ storageIds: storageId.array() }))
.query(async ({ input }) => {
return await getMultipleWithRelations({
db,
@ -79,8 +76,11 @@ export const storageRouter = createTRPCRouter({
}),
renameFile: adminProcedure
.input(z.object({ fileId: z.string(), name: z.string() }))
.input(z.object({ fileId: storageId, name: z.string() }))
.mutation(async ({ input }) => {
return await editFile(input.fileId, input.name);
return await editFile(input.fileId, { filename: input.name });
}),
getFile: publicProcedure.input(storageId).query(async ({ input }) => {
return await getFile(input);
}),
});

View file

@ -169,7 +169,7 @@ export const getAnnuncioData = async ({
const ogImage =
annuncio.images && annuncio.images.length > 0 && annuncio.images[0]
? getStorageUrl({
storageId: annuncio.images[0].img,
id: annuncio.images[0].img,
params: {
cacheKey: annuncio.media_updated_at?.toISOString(),
media: "image",
@ -411,7 +411,9 @@ export const getAnnunciRicerca = async ({
query = query.orderBy("id", "asc");
return await query.execute();
const a = await query.execute();
console.log(a[0]);
return a;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",

View file

@ -5,6 +5,7 @@ import { z } from "zod/v4";
import { env } from "~/env";
import { withParsedDates } from "~/lib/dateParserJsonKysely";
import type { Annunci, AnnunciId } from "~/schemas/public/Annunci";
import type { ImagesRefs } from "~/schemas/public/ImagesRefs";
import type { Ordini, OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { Prezziario } from "~/schemas/public/Prezziario";
import type {
@ -13,11 +14,13 @@ import type {
ServizioUpdate,
} from "~/schemas/public/Servizio";
import type { ServizioAnnunci } from "~/schemas/public/ServizioAnnunci";
import type { StorageId } from "~/schemas/public/Storage";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import type TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { Users, UsersId } from "~/schemas/public/Users";
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import type { VideosRefs } from "~/schemas/public/VideosRefs";
import {
addEventToQueue,
canSendNotification,
@ -325,7 +328,7 @@ export const AdminAttivaServizio_Ufficio = async (
};
type DocRef = {
storageId: string;
storage_id: StorageId | null;
ref: UsersStorageUserStorageId | null;
};
@ -355,14 +358,8 @@ export type ServizioAnnuncioData = ServizioAnnunci &
| "persone"
| "permanenza"
> & {
images: {
img: string;
thumb: string;
}[];
videos: {
thumb: string;
video: string;
}[];
images: Pick<ImagesRefs, "img" | "thumb">[];
videos: Pick<VideosRefs, "video" | "thumb">[];
};
export type ServizioData = Servizio & {
doc_personale_fronte: DocRef | null;
@ -400,7 +397,7 @@ export const getAllServizioAnnunci = async (
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"users_anagrafica.doc_personale_fronte_ref as ref",
])
.whereRef(
@ -413,7 +410,7 @@ export const getAllServizioAnnunci = async (
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"users_anagrafica.doc_personale_retro_ref as ref",
])
.whereRef(
@ -426,7 +423,7 @@ export const getAllServizioAnnunci = async (
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"servizio.doc_motivazione_ref as ref",
])
.whereRef(
@ -546,7 +543,7 @@ export const getServizioDataById = async (
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"users_anagrafica.doc_personale_fronte_ref as ref",
])
.whereRef(
@ -559,7 +556,7 @@ export const getServizioDataById = async (
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"users_anagrafica.doc_personale_retro_ref as ref",
])
.whereRef(
@ -572,7 +569,7 @@ export const getServizioDataById = async (
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"servizio.doc_motivazione_ref as ref",
])
.whereRef(

View file

@ -1,84 +1,69 @@
import { TRPCError } from "@trpc/server";
import { sql } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/postgres";
import type { MessagesMessageid } from "~/schemas/public/Messages";
import type { StorageId } from "~/schemas/public/Storage";
import type { UsersId } from "~/schemas/public/Users";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
import { db, type Querier } from "~/server/db";
import { fetchFiles, retriveFilesSafely } from "../services/storage.service";
export const getMultipleWithRelations = async ({
db,
ids,
}: {
db: Querier;
ids: string[];
ids: StorageId[];
}) => {
const files = await fetchFiles();
const userStorage = await db
.$pickTables<"users_storage" | "users">()
.selectFrom("users_storage")
.selectAll("users_storage")
.innerJoin("users", "users.id", "users_storage.userId")
.select("users.username")
.where(
"storageId",
"in",
files.map((f) => f.id),
)
.execute();
return ids
.map((id) => {
const file = files.find((f) => f.id === id);
if (!file) return null;
const users = userStorage
.filter((us) => us.storageId === id)
.map((us) => ({
id: us.userId,
username: us.username,
}));
return {
...file,
users,
};
})
.filter((f) => f !== null);
try {
return await db
.$pickTables<"users_storage" | "users" | "storage">()
.selectFrom("storage")
.select([
"storage.id",
"storage.created_at",
"storage.expires_at",
"storage.file_size_bytes",
"storage.filename",
"storage.mime_type",
"storage.tag",
])
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("users_storage")
.innerJoin("users", "users.id", "users_storage.userId")
.select(["users.username", "users.id"])
.whereRef("users_storage.storage_id", "=", "storage.id"),
).as("users"),
])
.where("storage.id", "in", ids)
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error retriving users-storage relations: ${(e as Error).message}`,
});
}
};
export const retrieveUserFiles = async ({ userId }: { userId: UsersId }) => {
try {
//all files linked to the user
const files = await db
.$pickTables<"users_storage">()
return await db
.$pickTables<"users_storage" | "storage">()
.selectFrom("users_storage")
.select(["storageId", "from_admin"])
.where("userId", "=", userId)
.select("users_storage.from_admin")
.innerJoin("storage", "storage.id", "users_storage.storage_id")
.select([
"storage.id",
"storage.created_at",
"storage.expires_at",
"storage.file_size_bytes",
"storage.filename",
"storage.mime_type",
"storage.tag",
])
.where("users_storage.userId", "=", userId)
.execute();
const { available, unavailable } = await retriveFilesSafely(
files.map((f) => f.storageId),
);
//remove entries in users_storage for files that no longer exist in storage
await db
.$pickTables<"users_storage">()
.deleteFrom("users_storage")
.where("userId", "=", userId)
.where("storageId", "in", unavailable)
.execute();
//merge file metadata with users_storage info
const userFiles = files
.map((uf) => {
const file = available.find((f) => f.id === uf.storageId);
if (!file) return null;
return {
...file,
from_admin: uf.from_admin,
};
})
.filter((f) => f !== null);
return userFiles;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@ -93,31 +78,27 @@ export const retrieveChatMessageFiles = async ({
messageId: MessagesMessageid;
}) => {
try {
const messageFiles = await db
.$pickTables<"messages_attachments" | "users_storage">()
return await db
.$pickTables<"messages_attachments" | "storage" | "users_storage">()
.selectFrom("messages_attachments")
.innerJoin(
"users_storage",
"users_storage.user_storage_id",
"messages_attachments.userStorageId",
)
.select("users_storage.storageId")
.innerJoin("storage", "storage.id", "users_storage.storage_id")
.select([
"storage.id",
"storage.created_at",
"storage.expires_at",
"storage.file_size_bytes",
"storage.filename",
"storage.mime_type",
"storage.tag",
])
.orderBy("messages_attachments.messageId", "asc")
.where("messages_attachments.messageId", "=", messageId)
.execute();
const { available, unavailable } = await retriveFilesSafely(
messageFiles.map((f) => f.storageId),
);
//remove entries in users_storage for files that no longer exist in storage
await db
.$pickTables<"users_storage">()
.deleteFrom("users_storage")
.where("storageId", "in", unavailable)
.execute();
return available;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@ -128,22 +109,22 @@ export const retrieveChatMessageFiles = async ({
export const getAllWithFromAdmin = async () => {
try {
const files = await db
.$pickTables<"users_storage">()
.selectFrom("users_storage")
.select(["storageId", "from_admin"])
return await db
.$pickTables<"users_storage" | "storage">()
.selectFrom("storage")
.leftJoin("users_storage", "users_storage.storage_id", "storage.id")
.select([
"storage.id",
"storage.created_at",
"storage.expires_at",
"storage.file_size_bytes",
"storage.filename",
"storage.mime_type",
"storage.tag",
sql<boolean>`coalesce(users_storage.from_admin, true)`.as("from_admin"),
])
.where("tag", "is", null)
.execute();
const allFiles = await fetchFiles();
const allFilesWithFromAdmin = allFiles.map((file) => {
const userStorageEntry = files.find((f) => f.storageId === file.id);
return {
...file,
from_admin: userStorageEntry ? userStorageEntry.from_admin : true,
};
});
return allFilesWithFromAdmin;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@ -158,7 +139,7 @@ export const addUserStorage = async (data: NewUsersStorage) => {
.$pickTables<"users_storage">()
.insertInto("users_storage")
.values(data)
.onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing())
.onConflict((oc) => oc.columns(["userId", "storage_id"]).doNothing())
.returning("user_storage_id")
.executeTakeFirst();
} catch (e) {
@ -174,14 +155,14 @@ export const deleteUserStorageEntry = async ({
storageId,
}: {
userId: UsersId;
storageId: string;
storageId: StorageId;
}) => {
try {
await db
.$pickTables<"users_storage">()
.deleteFrom("users_storage")
.where("userId", "=", userId)
.where("storageId", "=", storageId)
.where("storage_id", "=", storageId)
.execute();
return "ok";
} catch (e) {
@ -195,13 +176,13 @@ export const deleteUserStorageEntry = async ({
export const purgeFileFromUserStorages = async ({
storageId,
}: {
storageId: string;
storageId: StorageId;
}) => {
try {
await db
.$pickTables<"users_storage">()
.deleteFrom("users_storage")
.where("storageId", "=", storageId)
.where("storage_id", "=", storageId)
.execute();
return "ok";
} catch (e) {

View file

@ -23,7 +23,7 @@ const pool = new Pool({
max: 10,
password: env.POSTGRES_PASSWORD,
port: Number.parseInt(env.PGPORT),
user: env.POSTGRES_USER
user: env.POSTGRES_USER,
});
pool.on("error", (err) => {
@ -83,4 +83,4 @@ const _DbLogger = (event: LogEvent) => {
}
};
export const pgPool = pool
export const pgPool = pool;

View file

@ -1,209 +1,142 @@
import z from "zod";
import { env } from "~/env";
const FileMetadataSchema = z.object({
id: z.string(),
originalName: z.string(),
size: z.number(),
mimeType: z.string(),
blockCount: z.number(),
uploadedAt: z.string(),
expiresAt: z.string().nullable(),
});
export type FileMetadata = z.infer<typeof FileMetadataSchema>;
import { TRPCError } from "@trpc/server";
import type {
Storage,
StorageId,
StorageUpdate,
} from "~/schemas/public/Storage";
import type { UsersId } from "~/schemas/public/Users";
import { db } from "~/server/db";
export type FileMetadata = Omit<Storage, "file_data">;
export type FileMetadataWithAdmin = FileMetadata & {
from_admin: boolean;
};
// Helper to fetch the list of all files
export async function fetchFiles(): Promise<FileMetadata[]> {
export const getFileInfo = async (fileId: StorageId) => {
try {
const response = await fetch(
`${env.STORAGE_URL}/bucket/storage?token=${env.STORAGE_TOKEN}`,
);
if (!response.ok) {
console.error("Failed to fetch file list:", response.statusText);
return [];
}
const parse = FileMetadataSchema.array().safeParse(await response.json());
if (parse.success) {
return parse.data;
}
console.error("Failed to parse file metadata:", parse.error);
return [];
} catch (error) {
console.error("Error fetching file list:", error);
return [];
const file = await db
.$pickTables<"storage">()
.selectFrom("storage")
.select([
"id",
"created_at",
"expires_at",
"file_size_bytes",
"filename",
"mime_type",
"tag",
])
.where("id", "=", fileId)
.executeTakeFirst();
return file;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to get file info: ${(e as Error).message}`,
});
}
}
};
export async function getFileInfo(id: string): Promise<FileMetadata | null> {
export const uploadFile = async ({
buffer,
filename,
mime_type,
expires_at,
userId,
tag,
}: {
buffer: Buffer;
filename: string;
mime_type: string;
expires_at?: Date;
userId?: UsersId;
tag?: string;
}) => {
try {
const response = await fetch(
`${env.STORAGE_URL}/info/${id}?token=${env.STORAGE_TOKEN}`,
);
if (!response.ok) {
console.error("Failed to fetch file list:", response.statusText);
return null;
}
const parse = FileMetadataSchema.safeParse(await response.json());
if (parse.success) {
return parse.data;
}
console.error("Failed to parse file metadata:", parse.error);
return null;
} catch (error) {
console.error("Error fetching file list:", error);
return null;
}
}
type EditResponse =
| {
success: true;
id: string;
}
| {
success: false;
error: string;
};
export async function editFile(
id: string,
filename?: string,
expiresAt?: string,
): Promise<EditResponse> {
const formData = new FormData();
formData.append("id", id);
if (expiresAt) {
formData.append("expires_at", expiresAt);
}
if (filename) {
formData.append("filename", filename);
}
try {
const response = await fetch(
`${env.STORAGE_URL}/edit?token=${env.STORAGE_TOKEN}`,
{
method: "POST",
body: formData,
},
);
if (response.ok) {
const res = await response.json();
const parsed = z
.object({
id: z.string(),
const storage = await db
.$pickTables<"storage">()
.insertInto("storage")
.values({
filename,
mime_type,
expires_at,
file_data: buffer,
file_size_bytes: buffer.length,
tag,
})
.returning("id")
.executeTakeFirstOrThrow();
if (userId) {
const userStorageId = await db
.$pickTables<"users_storage">()
.insertInto("users_storage")
.values({
userId,
storage_id: storage.id,
})
.parse(res);
const fileID = parsed.id;
return { success: true, id: fileID };
.returning("user_storage_id")
.executeTakeFirstOrThrow();
return {
storageId: storage.id,
userStorageId: userStorageId.user_storage_id,
};
}
return {
success: false,
error: `Modifica fallita con status: ${response.status}`,
storageId: storage.id,
userStorageId: null,
};
} catch (e) {
return {
success: false,
error: `Errore Modifica file: ${(e as Error).message}`,
};
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to upload file: ${(e as Error).message}`,
});
}
}
};
type DeleteResponse =
| {
success: true;
}
| {
success: false;
error: string;
};
// Helper to handle file deletion
export async function deleteFile(fileID: string): Promise<DeleteResponse> {
export const getFile = async (fileId: StorageId) => {
try {
// The Go server accepts POST with form data for delete
const response = await fetch(
`${env.STORAGE_URL}/delete/${fileID}?token=${env.STORAGE_TOKEN}`,
{
method: "DELETE",
},
);
// Treat redirect as success
if (response.ok || (response.status >= 300 && response.status < 400)) {
return { success: true };
}
return {
success: false,
error: `Eliminazione fallita con status: ${response.status}`,
};
} catch (error) {
console.error("Error during file deletion:", error);
return { success: false, error: (error as Error).message };
}
}
// export const handleDownload = async (file: FileMetadata) => {
// try {
// const response = await fetch(
// `${env.STORAGE_URL}/get/${file.id}?token=${env.STORAGE_TOKEN}&mode=download`,
// );
// if (!response.ok) {
// throw new Error("File fetch failed");
// }
// const blob = await response.blob();
// const url = window.URL.createObjectURL(blob);
// const a = document.createElement("a");
// a.href = url;
// a.download = file.originalName;
// document.body.appendChild(a);
// a.click();
// a.remove();
// window.URL.revokeObjectURL(url);
// } catch {
// throw new Error("File download failed");
// }
// };
const safeRetrive = z.object({
available: z.array(FileMetadataSchema),
unavailable: z.array(z.string()),
});
export async function retriveFilesSafely(ids: string[]) {
try {
const response = await fetch(
`${env.STORAGE_URL}/files-check?token=${env.STORAGE_TOKEN}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(ids),
},
);
if (!response.ok) {
throw new Error("File fetch failed");
}
const parse = safeRetrive.safeParse(await response.json());
if (parse.success) {
return parse.data;
}
throw new Error("File fetch parse failed");
return await db
.$pickTables<"storage">()
.selectFrom("storage")
.selectAll()
.where("id", "=", fileId)
.executeTakeFirst();
} catch (e) {
throw new Error(`File fetch failed: ${(e as Error).message}`);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to get file: ${(e as Error).message}`,
});
}
}
};
export const editFile = async (fileId: StorageId, data: StorageUpdate) => {
try {
await db
.$pickTables<"storage">()
.updateTable("storage")
.set(data)
.where("id", "=", fileId)
.execute();
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to update file info: ${(e as Error).message}`,
});
}
};
export const deleteFile = async (fileId: StorageId) => {
try {
await db
.$pickTables<"storage">()
.deleteFrom("storage")
.where("id", "=", fileId)
.execute();
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to delete file info: ${(e as Error).message}`,
});
}
};

View file

@ -147,7 +147,7 @@ export const getClientProfilo = async ({
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"users_anagrafica.doc_personale_fronte_ref as ref",
])
.whereRef(
@ -160,7 +160,7 @@ export const getClientProfilo = async ({
eb
.selectFrom("users_storage")
.select([
"users_storage.storageId as storageId",
"users_storage.storage_id",
"users_anagrafica.doc_personale_retro_ref as ref",
])
.whereRef(

View file

@ -106,6 +106,7 @@ export const api = createTRPCNext<AppRouter>({
},
};
},
/**
* Whether tRPC should await queries when server rendering pages.
*/