mega update storage
This commit is contained in:
parent
c517dbcc7a
commit
48b31a5491
77 changed files with 2037 additions and 1716 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
68
apps/backend/gen/postgres/postgres/public/model/annunci.go
Normal file
68
apps/backend/gen/postgres/postgres/public/model/annunci.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
24
apps/backend/gen/postgres/postgres/public/model/storage.go
Normal file
24
apps/backend/gen/postgres/postgres/public/model/storage.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
231
apps/backend/gen/postgres/postgres/public/table/annunci.go
Normal file
231
apps/backend/gen/postgres/postgres/public/table/annunci.go
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
99
apps/backend/gen/postgres/postgres/public/table/storage.go
Normal file
99
apps/backend/gen/postgres/postgres/public/table/storage.go
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
3
apps/backend/jet.sh
Normal 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
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
92
apps/backend/queries/storage.go
Normal file
92
apps/backend/queries/storage.go
Normal 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
|
||||
|
||||
}
|
||||
72
apps/backend/queries/utils.go
Normal file
72
apps/backend/queries/utils.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
190
apps/db/migrations/56_storage.up.sql
Normal file
190
apps/db/migrations/56_storage.up.sql
Normal 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 $$;
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
"ignoreDependencies": [
|
||||
"pdfjs-dist",
|
||||
"kysely-plugin-serialize",
|
||||
"sharp",
|
||||
"@react-email/components",
|
||||
"kanel-kysely",
|
||||
"kanel-zod",
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
51
apps/infoalloggi/package-lock.json
generated
51
apps/infoalloggi/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -327,9 +327,10 @@ const AnnunciList = () => {
|
|||
maxBudget: maxBudget || undefined,
|
||||
},
|
||||
{
|
||||
gcTime: 60 * 1000,
|
||||
enabled: !map && !isUpdatingRicerca,
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 5000,
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
144
apps/infoalloggi/src/pages/api/storage/get/[id].ts
Normal file
144
apps/infoalloggi/src/pages/api/storage/get/[id].ts
Normal 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,
|
||||
},
|
||||
};
|
||||
80
apps/infoalloggi/src/pages/api/storage/upload.ts
Normal file
80
apps/infoalloggi/src/pages/api/storage/upload.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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[]>([]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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: "/",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
65
apps/infoalloggi/src/schemas/public/Storage.ts
Normal file
65
apps/infoalloggi/src/schemas/public/Storage.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export const api = createTRPCNext<AppRouter>({
|
|||
},
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether tRPC should await queries when server rendering pages.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue