diff --git a/client/src/components/package-details/PackageDetailsTable.tsx b/client/src/components/package-details/PackageDetailsTable.tsx index 905cf9fb..93fdf2e8 100644 --- a/client/src/components/package-details/PackageDetailsTable.tsx +++ b/client/src/components/package-details/PackageDetailsTable.tsx @@ -33,6 +33,8 @@ const Entry: FC<{ ) +const UNKNOWN_DATE_SENTINEL = '0001-01-01T00:00:00Z' + const PackageDetailsTable: FC<{ data: PackageInfo dependencyCount: number @@ -62,6 +64,14 @@ const PackageDetailsTable: FC<{ + + {data.lastUpdatedAt === UNKNOWN_DATE_SENTINEL + ? '-' + : new Intl.DateTimeFormat().format( + new Date(data.lastUpdatedAt), + )} + + {dependencyCount ? displayNumber(dependencyCount) diff --git a/client/src/locale/en-US.locale.ts b/client/src/locale/en-US.locale.ts index 444ce17f..70b0412c 100644 --- a/client/src/locale/en-US.locale.ts +++ b/client/src/locale/en-US.locale.ts @@ -100,6 +100,7 @@ export default { maintainer: 'Maintainer', dependencies: 'Dependencies', requiredBy: 'Required By', + lastUpdatedAt: 'Last Updated At', }, orphaned: 'Orphaned', noResults: 'None', diff --git a/client/src/locale/es-ES.locale.ts b/client/src/locale/es-ES.locale.ts index 312a259d..788cda8f 100644 --- a/client/src/locale/es-ES.locale.ts +++ b/client/src/locale/es-ES.locale.ts @@ -103,6 +103,7 @@ export default { maintainer: 'Mantenedor', dependencies: 'Dependencias', requiredBy: 'Requierdo por', + lastUpdatedAt: 'Última actualización', }, orphaned: 'Huérfanos', noResults: 'Nada', @@ -120,6 +121,7 @@ export default { runtimeDependencies: 'Dependencias del tiempo de Ejecución', pacstallDependencies: 'Dependencias de Pacstall', name: 'Nombre', + version: 'Versión', close: 'Cerrar', provider: 'Proveedor', noDescription: 'No hay descripción disponible', diff --git a/client/src/locale/fr-FR.locale.ts b/client/src/locale/fr-FR.locale.ts index 017cd6be..44e92c2b 100644 --- a/client/src/locale/fr-FR.locale.ts +++ b/client/src/locale/fr-FR.locale.ts @@ -102,6 +102,7 @@ export default { maintainer: 'Mainteneur', dependencies: 'Dépendances', requiredBy: 'Requis par', + lastUpdatedAt: 'Dernière mise à jour', }, orphaned: 'Orphelin', noResults: 'Aucun', @@ -122,6 +123,7 @@ export default { close: 'Fermer', provider: 'Fournisseur', noDescription: 'Aucune description disponible', + version: 'Version', }, requiredByModal: { title: 'Requis par', diff --git a/client/src/locale/id-ID.locale.ts b/client/src/locale/id-ID.locale.ts index 0ac7c248..f630ad34 100644 --- a/client/src/locale/id-ID.locale.ts +++ b/client/src/locale/id-ID.locale.ts @@ -101,6 +101,7 @@ export default { maintainer: 'Pemelihara', dependencies: 'Dependensi', requiredBy: 'Dibutuhkan oleh', + lastUpdatedAt: 'Diperbarui pada', }, orphaned: 'Ditelantarkan', noResults: 'Tidak ditemukan', @@ -121,6 +122,7 @@ export default { close: 'Tutup', provider: 'Penyedia', noDescription: 'Deskripsi tidak tersedia', + version: 'Versi', }, requiredByModal: { title: 'Disediakan oleh', diff --git a/client/src/locale/it-IT.locale.ts b/client/src/locale/it-IT.locale.ts index 787bee95..968513a8 100644 --- a/client/src/locale/it-IT.locale.ts +++ b/client/src/locale/it-IT.locale.ts @@ -101,6 +101,7 @@ export default { maintainer: 'Mantenitore', dependencies: 'Dipendenze', requiredBy: 'Richiesto da', + lastUpdatedAt: 'Ultimo aggiornamento', }, orphaned: 'Orfano', noResults: 'Nessuno', @@ -121,6 +122,7 @@ export default { close: 'Chiudi', provider: 'Fornitore', noDescription: 'Nessuna descrizione disponibile', + version: 'Versione', }, requiredByModal: { title: 'Richiesto da', diff --git a/client/src/locale/locale.ts b/client/src/locale/locale.ts index 789e109b..103cd4b8 100644 --- a/client/src/locale/locale.ts +++ b/client/src/locale/locale.ts @@ -103,6 +103,7 @@ export default interface Locale { maintainer: string dependencies: string requiredBy: string + lastUpdatedAt: string } noResults: string orphaned: string diff --git a/client/src/locale/nl-NL.locale.ts b/client/src/locale/nl-NL.locale.ts index 20475a79..85754758 100644 --- a/client/src/locale/nl-NL.locale.ts +++ b/client/src/locale/nl-NL.locale.ts @@ -101,6 +101,7 @@ export default { maintainer: 'Onderhouder', dependencies: 'Afhankelijkheden', requiredBy: 'Vereist Door', + lastUpdatedAt: 'Laatst Bijgewerkt Op', }, orphaned: 'Verweesd', noResults: 'Geen', @@ -121,6 +122,7 @@ export default { close: 'Sluiten', provider: 'Aanbieder', noDescription: 'Geen omschrijving beschikbaar', + version: 'Versie', }, requiredByModal: { title: 'Vereist Door', diff --git a/client/src/locale/pl-PL.locale.ts b/client/src/locale/pl-PL.locale.ts index 4e9d0e90..11a02817 100644 --- a/client/src/locale/pl-PL.locale.ts +++ b/client/src/locale/pl-PL.locale.ts @@ -102,6 +102,7 @@ export default { maintainer: 'Twórca', dependencies: 'Zależności', requiredBy: 'Wymagany przez', + lastUpdatedAt: 'Ostatnio zaktualizowany', }, orphaned: 'Nie utrzymywany', noResults: 'Brak', @@ -122,6 +123,7 @@ export default { close: 'Zamknij', provider: 'Dostawca', noDescription: 'Brak opisu', + version: 'Wersja', }, requiredByModal: { title: 'Wymagany przez', diff --git a/client/src/locale/pt-BR.locale.ts b/client/src/locale/pt-BR.locale.ts index 6e4c07af..82589af6 100644 --- a/client/src/locale/pt-BR.locale.ts +++ b/client/src/locale/pt-BR.locale.ts @@ -100,6 +100,7 @@ export default { maintainer: 'Mantenedor', dependencies: 'Dependências', requiredBy: 'Requerido Por', + lastUpdatedAt: 'Última Atualização', }, orphaned: 'Órfão', noResults: 'Nenhum', diff --git a/client/src/locale/pt-PT.locale.ts b/client/src/locale/pt-PT.locale.ts index 4b21d02f..729fdfcf 100644 --- a/client/src/locale/pt-PT.locale.ts +++ b/client/src/locale/pt-PT.locale.ts @@ -100,6 +100,7 @@ export default { maintainer: 'Responsável', dependencies: 'Dependências', requiredBy: 'Requerido Por', + lastUpdatedAt: 'Última Atualização', }, orphaned: 'Órfão', noResults: 'Nenhum', diff --git a/client/src/locale/ro-RO.locale.ts b/client/src/locale/ro-RO.locale.ts index 89ae6c35..9a5416dc 100644 --- a/client/src/locale/ro-RO.locale.ts +++ b/client/src/locale/ro-RO.locale.ts @@ -101,6 +101,7 @@ export default { maintainer: 'Menținut de', dependencies: 'Dependențe', requiredBy: 'Necesar pentru', + lastUpdatedAt: 'Ultima actualizare', }, orphaned: 'Nemenținut', noResults: 'Fără', @@ -121,6 +122,7 @@ export default { close: 'Închide', provider: 'Furnizor', noDescription: 'Nu există o descriere disponibilă', + version: 'Versiune', }, requiredByModal: { title: 'Necesar pentru', diff --git a/client/src/locale/sv-SE.locale.ts b/client/src/locale/sv-SE.locale.ts index e6065d51..913486b9 100644 --- a/client/src/locale/sv-SE.locale.ts +++ b/client/src/locale/sv-SE.locale.ts @@ -101,6 +101,7 @@ export default { maintainer: 'Underhållare', dependencies: 'Beroenden', requiredBy: 'Krävs av', + lastUpdatedAt: 'Senast uppdaterad', }, orphaned: 'Oandvänd', noResults: 'Inga resultat', @@ -121,6 +122,7 @@ export default { close: 'Stäng', provider: 'Leverantör', noDescription: 'Ingen beskrivning finns', + version: 'Version', }, requiredByModal: { title: 'Krävs av', diff --git a/client/src/locale/tr-TR.locale.ts b/client/src/locale/tr-TR.locale.ts index 51238023..7860bf9c 100644 --- a/client/src/locale/tr-TR.locale.ts +++ b/client/src/locale/tr-TR.locale.ts @@ -101,6 +101,7 @@ export default { maintainer: 'Bakıcı', dependencies: 'Gereksinimler', requiredBy: 'Taradından gerekli', + lastUpdatedAt: 'Son güncelleme', }, orphaned: 'Terk edilmiş', noResults: 'Hiçbirşey', @@ -121,6 +122,7 @@ export default { close: 'Kapat', provider: 'Sağlayıcı', noDescription: 'Açıklama mevcut değil', + version: 'Sürüm', }, requiredByModal: { title: 'Tarafından Gerekli', diff --git a/client/src/types/package-info.ts b/client/src/types/package-info.ts index 723a6808..e94a8a5f 100644 --- a/client/src/types/package-info.ts +++ b/client/src/types/package-info.ts @@ -19,6 +19,7 @@ export default interface PackageInfo { latestVersion?: string prettyName: string updateStatus: UpdateStatus + lastUpdatedAt: string } export enum UpdateStatus { diff --git a/server/Makefile b/server/Makefile index 51a7a13c..c3bc07a5 100644 --- a/server/Makefile +++ b/server/Makefile @@ -14,6 +14,17 @@ LDFLAGS= \ all: dist/webserver test: + PACSTALL_DATABASE_HOST=localhost \ + PACSTALL_DATABASE_PORT=3306 \ + PACSTALL_DATABASE_USER=root \ + PACSTALL_DATABASE_PASSWORD=changeme \ + PACSTALL_DATABASE_NAME=pacstall \ + PACSTALL_DISCORD_ENABLED=false \ + PACSTALL_DISCORD_TOKEN="" \ + PACSTALL_DISCORD_CHANNEL_ID="" \ + PACSTALL_DISCORD_TAGS="" \ + PACSTALL_MATOMO_ENABLED="false" \ + PACSTALL_REPOLOGY_ENABLED="false" \ GO_ENV=test go test -v ./... run: diff --git a/server/bin/webserver/main.go b/server/bin/webserver/main.go index e1b6b2df..97008335 100644 --- a/server/bin/webserver/main.go +++ b/server/bin/webserver/main.go @@ -59,24 +59,24 @@ func main() { printLogo() setupRequests() - log.Info("Registered http requests") + log.Info("registered http requests") - log.Info("Attempting to start TCP listener") + log.Info("attempting to start TCP listener") server.OnServerOnline(func() { - log.NotifyCustom("🚀 Startup 🧑‍🚀", "Successfully started up.") - log.Info("Server is now online on port %v.\n", config.Port) + log.NotifyCustom("🚀 Startup 🧑‍🚀", "successfully started up.") + log.Info("server is now online on port %v.\n", config.Port) - log.Info("Booted in %v\n", color.GreenString("%v", time.Since(startedAt))) + log.Info("booted in %v\n", color.GreenString("%v", time.Since(startedAt))) parser.ScheduleRefresh(config.UpdateInterval) - log.Info("Scheduled pacscripts to auto-refresh every %v", config.UpdateInterval) + log.Info("scheduled pacscripts to auto-refresh every %v", config.UpdateInterval) if config.Repology.Enabled { repology.ScheduleRefresh(config.RepologyUpdateInterval) - log.Info("Scheduled repology to auto-refresh every %v", config.RepologyUpdateInterval) + log.Info("scheduled repology to auto-refresh every %v", config.RepologyUpdateInterval) } else { - log.Warn("Repology is disabled. Pacstall will not be able to fetch package data from Repology.") + log.Warn("repository repology is disabled") } }) diff --git a/server/consts/consts.go b/server/consts/consts.go new file mode 100644 index 00000000..fa4a9d40 --- /dev/null +++ b/server/consts/consts.go @@ -0,0 +1,3 @@ +package consts + +const PACSCRIPT_FILE_EXTENSION = "pacscript" diff --git a/server/go.mod b/server/go.mod index 356da9d3..e86a4a73 100644 --- a/server/go.mod +++ b/server/go.mod @@ -25,6 +25,7 @@ require ( github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/joomcode/errorx v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/net v0.7.0 // indirect diff --git a/server/go.sum b/server/go.sum index eabe788e..4694a4e6 100644 --- a/server/go.sum +++ b/server/go.sum @@ -2,6 +2,8 @@ github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4Ho github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -23,6 +25,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joomcode/errorx v1.1.1 h1:/LFG/qSk1gUTuZjs+qlyOJEpcVjD9DXgBNFhdZkQrjY= +github.com/joomcode/errorx v1.1.1/go.mod h1:eQzdtdlNyN7etw6YCS4W4+lu442waxZYw5yvz0ULrRo= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -33,6 +37,7 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/treelightsoftware/go-matomo v0.1.0-beta h1:lTSvp3XCwEjS8allzntKLj6Hyd1HgYHn4SrbtMp0+s0= diff --git a/server/repology/fetch.go b/server/repology/fetch.go index 0bb996fe..71b29024 100644 --- a/server/repology/fetch.go +++ b/server/repology/fetch.go @@ -1,12 +1,12 @@ package repology import ( - "errors" "fmt" "strings" + "github.com/joomcode/errorx" "pacstall.dev/webserver/model" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" ) func parseRepologyFilter(filter string) (string, string) { @@ -30,7 +30,7 @@ func findRepologyProject(search []string) (model.RepologyProjectProvider, error) var result model.RepologyProjectProvider if len(search) == 0 { - return result, fmt.Errorf("no search terms provided") + return result, errorx.IllegalArgument.New("no search terms provided") } db := model.Instance() @@ -41,7 +41,7 @@ func findRepologyProject(search []string) (model.RepologyProjectProvider, error) filterName, filterValue := parseRepologyFilter(filter) column, ok := repologyFilterToColumn[filterName] if !ok { - return result, fmt.Errorf("invalid filter '%v'", filterName) + return result, errorx.IllegalArgument.New("invalid filter '%v'", filterName) } query = query.Where(fmt.Sprintf("%v = ?", column), filterValue) @@ -49,7 +49,7 @@ func findRepologyProject(search []string) (model.RepologyProjectProvider, error) var results []model.RepologyProjectProvider if err := query.Order("version desc").Find(&results).Error; err != nil || len(results) == 0 { - return result, errors.Join(errors.New("failed to fetch repology project"), err) + return result, errorx.Decorate(err, "failed to fetch repology project") } results = sortByStatus(results) @@ -72,7 +72,7 @@ var repologyStatusPriority = map[string]int{ } func sortByStatus(projects []model.RepologyProjectProvider) []model.RepologyProjectProvider { - return list.From(projects).SortBy(func(p1, p2 model.RepologyProjectProvider) bool { + return array.SortBy(array.Clone(projects), func(p1, p2 model.RepologyProjectProvider) bool { return repologyStatusPriority[p1.Status] < repologyStatusPriority[p2.Status] - }).ToSlice() + }) } diff --git a/server/repology/internal/exporter.go b/server/repology/internal/exporter.go index 4e986631..cbb1c12d 100644 --- a/server/repology/internal/exporter.go +++ b/server/repology/internal/exporter.go @@ -23,7 +23,7 @@ func ExportRepologyDatabase(db *gorm.DB) error { delay := makeSecondDelay() defer delay.Wait() - log.Debug("Page %v | Cursor at: %v", it, lastProjectName) + log.Debug("page %v | cursor at: %v", it, lastProjectName) projectPage, err := getProjectSearch(lastProjectName) if err != nil { return errors.Join(errors.New("failed to fetch repology project page"), err) diff --git a/server/repology/scheduler.go b/server/repology/scheduler.go index a4423c6d..48481bb3 100644 --- a/server/repology/scheduler.go +++ b/server/repology/scheduler.go @@ -11,12 +11,12 @@ func ScheduleRefresh(every time.Duration) { go func() { for { db := model.Instance() - log.Info("Refreshing Repology database...") + log.Info("refreshing Repology database...") err := ExportRepologyDatabase(db) if err != nil { - log.Error("Failed to export Repology projects: %v", err) + log.Error("failed to export Repology projects: %v", err) } else { - log.Info("Repology database refreshed successfully") + log.Info("repology database refreshed successfully") } time.Sleep(every) diff --git a/server/server/api/pacscripts/dependencies.go b/server/server/api/pacscripts/dependencies.go index e8a1dbb5..8f71ffdf 100644 --- a/server/server/api/pacscripts/dependencies.go +++ b/server/server/api/pacscripts/dependencies.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" "pacstall.dev/webserver/log" "pacstall.dev/webserver/server" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" "pacstall.dev/webserver/types/pac/pacstore" ) @@ -35,7 +36,10 @@ func GetPacscriptDependenciesHandle(w http.ResponseWriter, req *http.Request) { allPacscripts := pacstore.GetAll() - pacpkg, err := allPacscripts.FindByName(name) + pacpkg, err := array.FindBy(allPacscripts, func(s *pac.Script) bool { + return s.Name == name + }) + if err != nil { w.WriteHeader(404) return @@ -43,10 +47,10 @@ func GetPacscriptDependenciesHandle(w http.ResponseWriter, req *http.Request) { pacstallDependencies := make([]*pac.Script, 0) for _, pkg := range pacpkg.PacstallDependencies { - if found, err := pacstore.GetAll().FindBy(func(pi *pac.Script) bool { return pkg == pi.Name }); err == nil { + if found, err := array.FindBy(allPacscripts, func(pi *pac.Script) bool { return pkg == pi.Name }); err == nil { pacstallDependencies = append(pacstallDependencies, found) } else { - log.Error("Could not find pacstall dependency %s of package %s.\n", pkg, pacpkg.Name) + log.Error("could not find pacstall dependency %s of package %s.\n", pkg, pacpkg.Name) } } diff --git a/server/server/api/pacscripts/package.go b/server/server/api/pacscripts/package.go index b8df56d0..e6d6ac09 100644 --- a/server/server/api/pacscripts/package.go +++ b/server/server/api/pacscripts/package.go @@ -6,6 +6,8 @@ import ( "github.com/gorilla/mux" "pacstall.dev/webserver/server" + "pacstall.dev/webserver/types/array" + "pacstall.dev/webserver/types/pac" "pacstall.dev/webserver/types/pac/pacstore" ) @@ -21,7 +23,10 @@ func GetPacscriptHandle(w http.ResponseWriter, req *http.Request) { return // req is cached } - pkg, err := pacstore.GetAll().FindByName(name) + pkg, err := array.FindBy(pacstore.GetAll(), func(s *pac.Script) bool { + return s.Name == name + }) + if err != nil { w.WriteHeader(http.StatusNotFound) return diff --git a/server/server/api/pacscripts/package_list.go b/server/server/api/pacscripts/package_list.go index 05e6cecd..c35c1e41 100644 --- a/server/server/api/pacscripts/package_list.go +++ b/server/server/api/pacscripts/package_list.go @@ -26,7 +26,7 @@ type packageListPage struct { func GetPacscriptListHandle(w http.ResponseWriter, req *http.Request) { - packages := pacstore.GetAll().ToSlice() + packages := pacstore.GetAll() params, err := query. New(req). OptionalInt(parser.PageKey, 0). diff --git a/server/server/api/pacscripts/required_by.go b/server/server/api/pacscripts/required_by.go index 57c6f5b5..ddbba5e0 100644 --- a/server/server/api/pacscripts/required_by.go +++ b/server/server/api/pacscripts/required_by.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/mux" "pacstall.dev/webserver/server" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" "pacstall.dev/webserver/types/pac/pacstore" ) @@ -28,14 +28,17 @@ func GetPacscriptRequiredByHandle(w http.ResponseWriter, req *http.Request) { allPackages := pacstore.GetAll() - found, err := allPackages.FindByName(name) + found, err := array.FindBy(allPackages, func(p *pac.Script) bool { + return p.Name == name + }) + if err != nil { w.WriteHeader(404) return } - requiredBy := allPackages.Filter(func(p *pac.Script) bool { - return list.List[string](found.RequiredBy).Contains(list.Is(p.Name)) + requiredBy := array.Filter(allPackages, func(it *array.Iterator[*pac.Script]) bool { + return array.Contains(found.RequiredBy, array.Is(it.Value.Name)) }) server.Json(w, requiredBy) diff --git a/server/server/api/repology/repology.go b/server/server/api/repology/repology.go index e7b39284..82b8415e 100644 --- a/server/server/api/repology/repology.go +++ b/server/server/api/repology/repology.go @@ -5,15 +5,17 @@ import ( "net/http" "pacstall.dev/webserver/server" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" "pacstall.dev/webserver/types/pac/pacstore" ) func GetRepologyPackageListHandle(w http.ResponseWriter, req *http.Request) { - packages := pacstore.GetAll().Filter(func(s *pac.Script) bool { - return len(s.Version) > 0 - }).ToSlice() + packages := pacstore.GetAll() + + packages = array.Filter(packages, func(it *array.Iterator[*pac.Script]) bool { + return len(it.Value.Version) > 0 + }) etag := fmt.Sprintf("%v", pacstore.LastModified().UTC().String()) if server.ApplyHeaders(etag, w, req) { @@ -21,8 +23,8 @@ func GetRepologyPackageListHandle(w http.ResponseWriter, req *http.Request) { return } - results := list.Map(packages, func(_ int, p *pac.Script) repologyPackage { - return newRepologyPackage(*p) + results := array.SwitchMap(packages, func(it *array.Iterator[*pac.Script]) repologyPackage { + return newRepologyPackage(it.Value) }) server.Json(w, results) diff --git a/server/server/api/repology/types.go b/server/server/api/repology/types.go index 268594d3..cd07872f 100644 --- a/server/server/api/repology/types.go +++ b/server/server/api/repology/types.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "pacstall.dev/webserver/consts" "pacstall.dev/webserver/types/pac" ) @@ -25,7 +26,7 @@ type repologyPackage struct { Patches []string `json:"patches"` } -func newRepologyPackage(p pac.Script) repologyPackage { +func newRepologyPackage(p *pac.Script) repologyPackage { return repologyPackage{ Name: p.Name, VisibleName: p.PrettyName, @@ -34,7 +35,7 @@ func newRepologyPackage(p pac.Script) repologyPackage { Version: p.Version, URL: p.URL, Type: getType(p), - RecipeURL: fmt.Sprintf("https://raw.githubusercontent.com/pacstall/pacstall-programs/master/packages/%s/%s.pacscript", p.Name, p.Name), + RecipeURL: fmt.Sprintf("https://raw.githubusercontent.com/pacstall/pacstall-programs/master/packages/%s/%s.%s", p.Name, p.Name, consts.PACSCRIPT_FILE_EXTENSION), PackageDetailsURL: fmt.Sprintf("https://pacstall.dev/packages/%s", p.Name), Patches: p.Patch, } @@ -47,7 +48,7 @@ var pacTypes = map[string]string{ "-app": "AppImage", } -func getMaintainer(p pac.Script) maintainerDetails { +func getMaintainer(p *pac.Script) maintainerDetails { if !strings.Contains(p.Maintainer, "<") { return maintainerDetails{ Name: &p.Maintainer, @@ -64,7 +65,7 @@ func getMaintainer(p pac.Script) maintainerDetails { } } -func getType(p pac.Script) string { +func getType(p *pac.Script) string { for suffix, kind := range pacTypes { if strings.HasSuffix(p.Name, suffix) { return kind diff --git a/server/server/api/url_shortener/url_shortener.go b/server/server/api/url_shortener/url_shortener.go index e16077f1..a31576b1 100644 --- a/server/server/api/url_shortener/url_shortener.go +++ b/server/server/api/url_shortener/url_shortener.go @@ -84,6 +84,6 @@ func pingMatomoTracker(user, userAgent, urlRef, link string) { err := matomo.Send(params) if err != nil { - log.Warn("Failed to ping matomo tracker: %s", err) + log.Warn("failed to ping matomo tracker: %s", err) } } diff --git a/server/server/health_check.go b/server/server/health_check.go index bd765284..669e6e73 100644 --- a/server/server/health_check.go +++ b/server/server/health_check.go @@ -33,7 +33,7 @@ func triggerServerOnline(port int) { } if timeout <= 0 { - log.Fatal("TCP server bootstrapping timed out.") + log.Fatal("tcp server bootstrapping timed out.") } for _, handler := range onServerOnlineHandlers { diff --git a/server/server/query/main.go b/server/server/query/main.go index 80c61490..798cb9f5 100644 --- a/server/server/query/main.go +++ b/server/server/query/main.go @@ -1,10 +1,11 @@ package query import ( - "fmt" "net/http" "strconv" "strings" + + "github.com/joomcode/errorx" ) type Key = string @@ -96,7 +97,7 @@ func (q *Query) Parse() (*QueryResult, error) { for _, requiredStr := range q.requiredStrs { if !vals.Has(requiredStr) { - return nil, fmt.Errorf("missing required query parameter '%v'", requiredStr) + return nil, errorx.IllegalArgument.New("missing required query parameter '%v'", requiredStr) } stringParams[requiredStr] = vals.Get(requiredStr) @@ -104,12 +105,12 @@ func (q *Query) Parse() (*QueryResult, error) { for _, requiredInt := range q.requiredInts { if !vals.Has(requiredInt) { - return nil, fmt.Errorf("missing required query parameter '%v'", requiredInt) + return nil, errorx.IllegalArgument.New("missing required query parameter '%v'", requiredInt) } value, err := strconv.ParseInt(vals.Get(requiredInt), 10, 32) if err != nil { - return nil, fmt.Errorf("required query parameter '%v' is not int", requiredInt) + return nil, errorx.Decorate(err, "failed to parse required query parameter '%v' as int", requiredInt) } intParams[requiredInt] = int(value) @@ -117,7 +118,7 @@ func (q *Query) Parse() (*QueryResult, error) { for requiredEnum, enumValues := range q.requiredEnums { if !vals.Has(requiredEnum) { - return nil, fmt.Errorf("missing required query parameter '%v'", requiredEnum) + return nil, errorx.IllegalArgument.New("missing required query parameter '%v'", requiredEnum) } value := vals.Get(requiredEnum) @@ -130,7 +131,7 @@ func (q *Query) Parse() (*QueryResult, error) { } if !found { - return nil, fmt.Errorf("required query parameter '%v' has value '%v' but expected one of [%v]", requiredEnum, value, enumValues) + return nil, errorx.IllegalArgument.New("required query parameter '%v' has value '%v' but expected one of [%v]", requiredEnum, value, enumValues) } stringParams[requiredEnum] = value @@ -183,7 +184,7 @@ func (q *Query) Parse() (*QueryResult, error) { _, okInt = intParams[required] if !okStr && !okInt { - return nil, fmt.Errorf("constraint error: param '%v' requires '%v' but it does not exit", key, required) + return nil, errorx.IllegalArgument.New("param '%v' requires '%v' but it does not exit", key, required) } } diff --git a/server/server/sitemap.go b/server/server/sitemap.go index 0b91970b..7690a118 100644 --- a/server/server/sitemap.go +++ b/server/server/sitemap.go @@ -38,7 +38,7 @@ func generateStaticSiteMap() []SitemapEntry { } func generateDynamicSiteMap() []SitemapEntry { - packages := pacstore.GetAll().ToSlice() + packages := pacstore.GetAll() entries := make([]SitemapEntry, len(packages)) for idx, pkg := range packages { diff --git a/server/server/spa.go b/server/server/spa.go index 02d1eb3b..35e6a9d4 100644 --- a/server/server/spa.go +++ b/server/server/spa.go @@ -56,6 +56,6 @@ func serveIndexHtml(w http.ResponseWriter, r *http.Request, staticPath string) { w.Header().Add("Content-Type", "text/html") if template.Execute(w, templateData) != nil { - http.Error(w, "Could not execute template", http.StatusInternalServerError) + http.Error(w, "could not execute template", http.StatusInternalServerError) } } diff --git a/server/server/ssr/pacscript/package.go b/server/server/ssr/pacscript/package.go index e211605e..a36b3532 100644 --- a/server/server/ssr/pacscript/package.go +++ b/server/server/ssr/pacscript/package.go @@ -4,7 +4,10 @@ import ( "fmt" "regexp" + "pacstall.dev/webserver/consts" r "pacstall.dev/webserver/server/ssr" + "pacstall.dev/webserver/types/array" + "pacstall.dev/webserver/types/pac" "pacstall.dev/webserver/types/pac/pacstore" ) @@ -14,7 +17,10 @@ func registerPacscriptSSRData() { func(path string, groups []string) r.IndexTemplateData { name := groups[1] - pkg, err := pacstore.GetAll().FindByName(name) + pkg, err := array.FindBy(pacstore.GetAll(), func(s *pac.Script) bool { + return s.Name == name + }) + if err != nil { return r.GenerateDefaultIndexTemplateData() } @@ -38,13 +44,13 @@ func registerPacscriptSSRData() {

Maintainer: %s

Version: %s

URL

-

Source

+

Source

Find similar packages here.

- `, pkg.Name, pkg.Name, pkg.Description, pkg.Maintainer, pkg.Version, pkg.URL, pkg.Name, pkg.Name, pkg.PackageName), + `, pkg.Name, pkg.Name, pkg.Description, pkg.Maintainer, pkg.Version, pkg.URL, pkg.Name, pkg.Name, consts.PACSCRIPT_FILE_EXTENSION, pkg.PackageName), } }, ) diff --git a/server/server/webserver.go b/server/server/webserver.go index 674e8462..1602491a 100644 --- a/server/server/webserver.go +++ b/server/server/webserver.go @@ -41,7 +41,7 @@ func Listen(port int) { if config.Production { path, err := filepath.Abs(config.PublicDir) if err != nil { - log.Fatal("failed to find client public dir at path '%s'. err: %v", config.PublicDir, err) + log.Fatal("failed to find client public dir at path '%s'. err: %+v", config.PublicDir, err) } Router().PathPrefix("/").Handler(spaHandler{staticPath: path}) @@ -57,18 +57,18 @@ func Listen(port int) { err := serverInstance.ListenAndServe() if errors.Is(err, http.ErrServerClosed) { - log.Info("Http server stopped") + log.Info("http server stopped") } else { - log.Fatal("Could not start TCP listener on port %v. Got error: %v\n", port, err) + log.Fatal("could not start TCP listener on port %v. Got error: %+v\n", port, err) } } func Shutdown() { if serverInstance == nil { - log.Info("Server instance is already down") + log.Info("server instance is already down") } ctx := context.Background() serverInstance.Shutdown(ctx) - log.Info("Gracefully shutting down the http server") + log.Info("gracefully shutting down the http server") } diff --git a/server/types/array/array.go b/server/types/array/array.go new file mode 100644 index 00000000..024e6793 --- /dev/null +++ b/server/types/array/array.go @@ -0,0 +1,256 @@ +package array + +import ( + "sort" + "sync/atomic" + + "github.com/joomcode/errorx" +) + +type ComparisonFunc[T any] func(a, b T) bool + +func SortBy[T any](arr []T, isLessThan func(T, T) bool) []T { + sort.SliceStable(arr, func(i, j int) bool { + return isLessThan(arr[i], arr[j]) + }) + + return arr +} + +func IndexOf[T any](arr []T, filter Filterer[T]) (int, error) { + for idx, it := range arr { + if filter(&Iterator[T]{idx, it, arr}) { + return idx, nil + } + } + + return -1, errorx.IllegalState.New("object does not exist in list") +} + +func Find[T any](arr []T, filter Filterer[T]) (T, error) { + idx, err := IndexOf(arr, filter) + if err != nil { + var out T + return out, err + } + + return arr[idx], nil +} + +func Equals[T any](arr []T, other []T, compare ComparisonFunc[T]) bool { + if (arr == nil) != (other == nil) { + return false + } + + if len(arr) != len(other) { + return false + } + + for idx, it := range arr { + if !compare(it, other[idx]) { + return false + } + } + + return true +} + +func Distinct[T any](arr []T, isEq ComparisonFunc[T]) []T { + out := make([]T, 0) + for _, item := range arr { + exists := Contains(out, func(it *Iterator[T]) bool { return isEq(it.Value, item) }) + if !exists { + out = append(out, item) + } + } + + return out +} + +func IsEmpty[T any](arr []T) bool { + return len(arr) == 0 +} + +func FindBy[T any](arr []T, predicate func(T) bool) (T, error) { + for _, it := range arr { + if predicate(it) { + return it, nil + } + } + + var out T + return out, errorx.IllegalState.New("object does not exist in list") +} + +func Contains[T any](arr []T, filter Filterer[T]) bool { + _, err := IndexOf(arr, filter) + return err == nil +} + +func ContainsPtr[T any](arr []T, item *T) bool { + _, err := IndexOf(arr, func(it *Iterator[T]) bool { + return &it.Value == item + }) + return err == nil +} + +func IsSorted[T any](arr []T, isLessThan func(T, T) bool) bool { + for i := 1; i < len(arr); i++ { + if !isLessThan(arr[i-1], arr[i]) { + return false + } + } + + return true +} + +func Clone[T any](arr []T) []T { + return append(make([]T, 0), arr...) +} + +func Filter[T any](arr []T, predicate func(*Iterator[T]) bool) []T { + out := make([]T, 0) + for idx, it := range arr { + if predicate(&Iterator[T]{idx, it, arr}) { + out = append(out, it) + } + } + + return out +} + +func FilterPtr[T any](arr []T, predicate func(*PtrIterator[T]) bool) []T { + out := make([]T, 0) + for idx, it := range arr { + if predicate(&PtrIterator[T]{idx, &it, arr}) { + out = append(out, it) + } + } + + return out +} + +func All[T any](arr []T, predicate func(T) bool) bool { + for _, it := range arr { + if !predicate(it) { + return false + } + } + + return true +} + +func Any[T any](arr []T, predicate func(T) bool) bool { + passes := false + for _, it := range arr { + if predicate(it) { + passes = true + } + } + + return passes +} + +func ToBufChan[T any](arr []T, ch chan T) chan T { + left := int32(len(arr)) + for _, item := range arr { + go func(item T) { + ch <- item + atomic.AddInt32(&left, -1) + if left == 0 { + close(ch) + } + }(item) + } + return ch +} + +func ToChan[T any](arr []T) (ch chan T) { + return ToBufChan(arr, ch) +} + +func Last[T any](arr []T) (T, error) { + if IsEmpty(arr) { + var out T + return out, errorx.IllegalState.New("array is empty") + } + + return arr[len(arr)-1], nil +} + +type Iterator[T any] struct { + Index int + Value T + Array []T +} + +type PtrIterator[T any] struct { + Index int + Value *T + Array []T +} + +/* Maps the given array in place. */ +func Map[T any](arr []T, mapper func(it *Iterator[T]) T) []T { + for idx, item := range arr { + value := mapper(&Iterator[T]{idx, item, arr}) + arr[idx] = value + } + + return arr +} + +func SwitchMap[T any, E any](arr []T, mapper func(it *Iterator[T]) E) []E { + out := make([]E, len(arr)) + for idx, item := range arr { + value := mapper(&Iterator[T]{idx, item, arr}) + out[idx] = value + } + + return out +} + +/* Maps the given array in place. */ +func MapPtr[T any](arr []T, mapper func(it *PtrIterator[T]) T) []T { + for idx, item := range arr { + value := mapper(&PtrIterator[T]{idx, &item, arr}) + arr[idx] = value + } + + return arr +} + +func SwitchMapPtr[T any, E any](arr []T, mapper func(it *PtrIterator[T]) E) []E { + out := make([]E, len(arr)) + for idx, item := range arr { + value := mapper(&PtrIterator[T]{idx, &item, arr}) + out[idx] = value + } + + return out +} + +func ReduceIndex[T any, E any](arr []T, reducer func(int, T, E) E, accumulator E) E { + out := accumulator + for idx, item := range arr { + out = reducer(idx, item, out) + } + + return out +} + +func Reduce[T any, E any](arr []T, reducer func(T, E) E, accumulator E) E { + return ReduceIndex(arr, func(i int, t T, e E) E { + return reducer(t, e) + }, accumulator) +} + +/* Reverse the given array in place. */ +func Reverse[T any](arr []T) []T { + for i := 0; i < len(arr)/2; i++ { + j := len(arr) - i - 1 + arr[i], arr[j] = arr[j], arr[i] + } + + return arr +} diff --git a/server/types/array/array_test.go b/server/types/array/array_test.go new file mode 100644 index 00000000..63b00fe3 --- /dev/null +++ b/server/types/array/array_test.go @@ -0,0 +1,114 @@ +package array_test + +import ( + "testing" + + "pacstall.dev/webserver/types/array" +) + +func Test_Array_SortBy(t *testing.T) { + data := []int{1, 2, 3, 4, 5} + array. + SortBy(data, array.Asc[int]()) + + if !array.IsSorted(data, array.Asc[int]()) { + t.Errorf("Expected array to be sorted") + } +} + +func Test_Array_IsSorted(t *testing.T) { + data := []int{1, 2, 3, 4, 5} + + if !array.IsSorted(data, array.Asc[int]()) { + t.Errorf("Expected array to be sorted") + } +} + +func Test_Array_IsSorted_Fail(t *testing.T) { + data := []int{1, 2, 6, 4, 5} + + if array.IsSorted(data, array.Asc[int]()) { + t.Errorf("Expected array to not be sorted") + } +} + +func Test_Array_IsSorted_Desc(t *testing.T) { + data := []int{5, 4, 3, 2, 1} + + if !array.IsSorted(data, array.Desc[int]()) { + t.Errorf("Expected array to be sorted") + } +} + +func Test_Array_IsSorted_Desc_Fail(t *testing.T) { + data := []int{5, 4, 5, 2, 1} + + if array.IsSorted(data, array.Desc[int]()) { + t.Errorf("Expected list to not be sorted") + } +} + +func Test_Array_Filter(t *testing.T) { + data := []int{5, 4, 7, 7, 2, 1} + data = array.Filter(data, array.Not[int](7)) + + if len(data) != 4 { + t.Errorf("Expected 4, got %d", len(data)) + } +} + +func Test_Array_Filter_Inclussive(t *testing.T) { + data := []int{5, 7, 7, 2, 3, 1} + + data = array.Filter(data, array.Is(7)) + + if len(data) != 2 { + t.Errorf("Expected 2, got %d", len(data)) + } +} + +func Test_Array_Contains(t *testing.T) { + found := array.Contains([]int{5, 4, 7, 7, 2, 1}, array.Is(2)) + if !found { + t.Errorf("Expected to find 2") + } +} + +func Test_Array_Contains_Fail(t *testing.T) { + found := array.Contains([]int{5, 4, 7, 7, 2, 1}, array.Is(10)) + + if found { + t.Errorf("Expected to not find 10") + } +} + +func Test_Find(t *testing.T) { + ten, err := array.Find([]int{5, 4, 7, 7, 2, 10}, array.Is(10)) + + if err != nil { + t.Errorf("Expected to find 10") + } + + if ten != 10 { + t.Errorf("Expected 10, got %d", ten) + } +} + +func Test_Find_Fail(t *testing.T) { + _, err := array.Find([]int{5, 4, 7, 7, 2, 10}, array.Is(11)) + + if err == nil { + t.Errorf("Expected to not find 11") + } +} + +func Test_Distinct(t *testing.T) { + items := []int{4, 7, 10, 4, 7, 10} + + expected := []int{4, 7, 10} + + actualDistinct := array.Distinct(items, array.Eq[int]()) + if !array.Equals(actualDistinct, expected, array.Eq[int]()) { + t.Errorf("Expected %v, got %v", expected, items) + } +} diff --git a/server/types/array/sort.go b/server/types/array/sort.go new file mode 100644 index 00000000..3b47e2a9 --- /dev/null +++ b/server/types/array/sort.go @@ -0,0 +1,37 @@ +package array + +type Mapper[T any, E any] func(T) E +type Filterer[T any] func(*Iterator[T]) bool +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string +} + +func Asc[T Ordered]() ComparisonFunc[T] { + return func(a, b T) bool { + return a < b + } +} + +func Eq[T Ordered]() ComparisonFunc[T] { + return func(a, b T) bool { + return a == b + } +} + +func Desc[T Ordered]() ComparisonFunc[T] { + return func(a, b T) bool { + return a > b + } +} + +func Is[T Ordered](value T) Filterer[T] { + return func(it *Iterator[T]) bool { + return it.Value == value + } +} + +func Not[T Ordered](value T) Filterer[T] { + return func(it *Iterator[T]) bool { + return value != it.Value + } +} diff --git a/server/types/list/list_util.go b/server/types/list/list_util.go deleted file mode 100644 index e7c025a8..00000000 --- a/server/types/list/list_util.go +++ /dev/null @@ -1,358 +0,0 @@ -package list - -import ( - "fmt" - "sort" - "sync/atomic" -) - -type List[T any] []T - -type Ordered interface { - ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string -} - -type sorter[T any] struct { - list []T - less func(T, T) bool -} - -func (s *sorter[T]) Swap(i, j int) { - s.list[i], s.list[j] = s.list[j], s.list[i] -} - -func (s *sorter[T]) Len() int { - return len(s.list) -} - -func (s *sorter[T]) Less(i, j int) bool { - return s.less(s.list[i], s.list[j]) -} - -type ComparableList[T Ordered] struct { - List[T] -} - -func New[T any]() List[T] { - return From(make([]T, 0)) -} - -func From[T any](items []T) List[T] { - return items -} - -type ComparisonFunc[T any] func(a, b T) bool - -func (list List[T]) IndexOf(filter Filterer[T]) (int, error) { - for idx, it := range list.ToSlice() { - if filter(it) { - return idx, nil - } - } - - return -1, fmt.Errorf("object does not exist in list") -} - -func shallowEqual[T any](a, b T) bool { - return &a == &b -} - -func (list List[T]) Equals(other List[T], compare ComparisonFunc[T]) bool { - if (list == nil) != (other == nil) { - return false - } - - if len(list) != len(other) { - return false - } - - for idx, it := range list.ToSlice() { - if !compare(it, other[idx]) { - return false - } - } - - return true -} - -func (list List[T]) Distinct(isEq ComparisonFunc[T]) (out List[T]) { - out = New[T]() - for _, item := range list.ToSlice() { - exists := out.Contains(func(it T) bool { return isEq(it, item) }) - if !exists { - out = append(out, item) - } - } - - return out -} - -func (list List[T]) Append(item T) List[T] { - clone := list.Clone() - clone = append(clone, item) - return clone -} - -func (list List[T]) Remove(item *T) List[T] { - clone := list.RemoveFunc(func(a T) bool { - return shallowEqual(a, *item) - }) - - return clone -} - -func (list ComparableList[T]) Remove(item T) List[T] { - clone := list.RemoveFunc(func(a T) bool { - return a == item - }) - - return clone -} - -func (list List[T]) RemoveFunc(matches func(T) bool) List[T] { - clone := list.FilterIndex(func(_ int, t T) bool { - return !matches(t) - }) - - return clone -} - -func (list List[T]) IsEmpty() bool { - return list.Len() == 0 -} - -func (list List[T]) Find(filter Filterer[T]) (T, error) { - idx, err := list.IndexOf(filter) - if err != nil { - var out T - return out, err - } - - return list[idx], nil -} - -func (list List[T]) Last() (T, error) { - if list.IsEmpty() { - var out T - return out, fmt.Errorf("list is empty") - } - - return list[list.Len()-1], nil -} - -func (list List[T]) MapIndex(mapper func(int, T) T) List[T] { - out := make(List[T], list.Len()) - for idx, item := range list { - value := mapper(idx, item) - out[idx] = value - } - - return out -} - -func (list List[T]) Map(mapper func(T) T) List[T] { - return list.MapIndex(func(i int, t T) T { - return mapper(t) - }) -} - -func (list List[T]) MapExt(mapper func(T, List[T]) T) List[T] { - clone := list.Clone() - return list.MapIndex(func(i int, t T) T { - return mapper(t, clone) - }) -} - -func (list List[T]) Apply(mapper func([]T) []T) List[T] { - return mapper(list.Clone()) -} - -func Apply[T any, E any](list List[T], mapper func([]T) []E) List[E] { - return mapper(list.Clone()) -} - -func MapIndex[T any, E any](list List[T], mapper func(int, T) E) List[E] { - out := make(List[E], list.Len()) - for idx, item := range list { - value := mapper(idx, item) - out[idx] = value - } - - return out -} - -func Map[T any, E any](list List[T], mapper func(int, T) E) List[E] { - return MapIndex(list, func(i int, t T) E { - return mapper(i, t) - }) -} - -func ReduceIndex[T any, E any](list List[T], reducer func(int, T, E) E, accumulator E) E { - out := accumulator - for idx, item := range list { - out = reducer(idx, item, out) - } - - return out -} - -func Reduce[T any, E any](list List[T], reducer func(T, E) E, accumulator E) E { - return ReduceIndex(list, func(i int, t T, e E) E { - return reducer(t, e) - }, accumulator) -} - -func (list List[T]) Reverse() List[T] { - clone := list.Clone() - for i := clone.Len() - 1; i >= 0; i-- { - clone = append(clone, clone[i]) - } - - return clone -} - -func (list List[T]) FindBy(predicate func(T) bool) (T, error) { - for _, it := range list.ToSlice() { - if predicate(it) { - return it, nil - } - } - - var out T - return out, fmt.Errorf("object does not exist in list") -} - -func (list List[T]) Contains(filter Filterer[T]) bool { - _, err := list.IndexOf(filter) - return err == nil -} - -func (list List[T]) ContainsPtr(item *T) bool { - _, err := list.IndexOf(func(t T) bool { - return &t == item - }) - return err == nil -} - -func (list ComparableList[T]) Contains(item T) bool { - _, err := list.IndexOf(Is(item)) - return err == nil -} - -func (list List[T]) Len() int { - return len(list.ToSlice()) -} - -func (list List[T]) ToSlice() []T { - return []T(list) -} - -func (list List[T]) SortBy(isLessThan func(T, T) bool) List[T] { - a := list.Clone().ToSlice()[:] - sort.SliceStable(a, func(i, j int) bool { - return isLessThan(a[i], a[j]) - }) - - return a -} - -func quicksort[T any](a []T, isLessThan func(T, T) bool) List[T] { - if len(a) < 2 { - return a - } - - left, right := 0, len(a)-1 - - // Pick a pivot - pivotIndex := len(a) / 2 - - // Move the pivot to the right - a[pivotIndex], a[right] = a[right], a[pivotIndex] - - // Pile elements smaller than the pivot on the left - for i := range a { - if isLessThan(a[i], a[right]) { - a[i], a[left] = a[left], a[i] - left++ - } - } - - // Place the pivot after the last smaller element - a[left], a[right] = a[right], a[left] - - // Go down the rabbit hole - quicksort(a[:left], isLessThan) - quicksort(a[left+1:], isLessThan) - - return a -} - -func (list List[T]) IsSorted(isLessThan func(T, T) bool) bool { - for i := 1; i < list.Len(); i++ { - if !isLessThan(list[i-1], list[i]) { - return false - } - } - - return true -} - -func (list List[T]) Clone() List[T] { - return append(List[T](make([]T, 0)), list...) -} - -func (list List[T]) FilterIndex(predicate func(int, T) bool) List[T] { - out := make([]T, 0) - for idx, it := range list.ToSlice() { - if predicate(idx, it) { - out = append(out, it) - } - } - - return out -} - -func (list List[T]) Filter(predicate func(T) bool) List[T] { - return list.FilterIndex(func(i int, t T) bool { - return predicate(t) - }) -} - -func (list List[T]) All(predicate func(T) bool) bool { - for _, it := range list.ToSlice() { - if !predicate(it) { - return false - } - } - - return true -} - -func (list List[T]) Any(predicate func(T) bool) bool { - passes := false - for _, it := range list.ToSlice() { - if predicate(it) { - passes = true - } - } - - return passes -} - -func (list List[T]) ToBufChan(ch chan T) chan T { - left := int32(list.Len()) - for _, item := range list.ToSlice() { - go func(item T) { - ch <- item - atomic.AddInt32(&left, -1) - if left == 0 { - close(ch) - } - }(item) - } - return ch -} - -func (list List[T]) ToChan() (ch chan T) { - return list.ToBufChan(ch) -} diff --git a/server/types/list/list_util_test.go b/server/types/list/list_util_test.go deleted file mode 100644 index f15d5543..00000000 --- a/server/types/list/list_util_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package list_test - -import ( - "testing" - - "pacstall.dev/webserver/types/list" -) - -func Test_List_Append_Remove(t *testing.T) { - data := list.New[int](). - Append(1). - Append(2). - Append(3). - Append(4). - Append(5). - Append(6). - RemoveFunc(list.Is(6)) - - if data.Len() != 5 { - t.Errorf("Expected 5, got %d", data.Len()) - } - - if data[0] != 1 { - t.Errorf("Expected 1, got %d", data[0]) - } - - if data[1] != 2 { - t.Errorf("Expected 2, got %d", data[1]) - } - - if data[2] != 3 { - t.Errorf("Expected 3, got %d", data[2]) - } - - if data[3] != 4 { - t.Errorf("Expected 4, got %d", data[3]) - } - - if data[4] != 5 { - t.Errorf("Expected 5, got %d", data[4]) - } -} - -func Test_List_SortBy(t *testing.T) { - data := list.New[int](). - Append(5). - Append(3). - Append(6). - Append(2). - Append(1). - SortBy(list.Asc[int]()) - - if !data.IsSorted(list.Asc[int]()) { - t.Errorf("Expected list to be sorted") - } -} - -func Test_List_IsSorted(t *testing.T) { - data := list.New[int](). - Append(1). - Append(2). - Append(3). - Append(4). - Append(5) - - if !data.IsSorted(list.Asc[int]()) { - t.Errorf("Expected list to be sorted") - } -} - -func Test_List_IsSorted_Fail(t *testing.T) { - data := list.New[int](). - Append(1). - Append(2). - Append(6). - Append(4). - Append(5) - - if data.IsSorted(list.Asc[int]()) { - t.Errorf("Expected list to be sorted") - } -} - -func Test_List_IsSorted_Desc(t *testing.T) { - data := list.New[int](). - Append(5). - Append(4). - Append(3). - Append(2). - Append(1) - - if !data.IsSorted(list.Desc[int]()) { - t.Errorf("Expected list to be sorted") - } -} - -func Test_List_IsSorted_Desc_Fail(t *testing.T) { - data := list.New[int](). - Append(5). - Append(4). - Append(7). - Append(2). - Append(1) - - if data.IsSorted(list.Desc[int]()) { - t.Errorf("Expected list to be sorted") - } -} - -func Test_List_Filter(t *testing.T) { - data := list.New[int](). - Append(5). - Append(4). - Append(7). - Append(7). - Append(2). - Append(1). - Filter(list.Not(7)) - - if data.Len() != 4 { - t.Errorf("Expected 4, got %d", data.Len()) - } -} - -func Test_List_Filter_Inclussive(t *testing.T) { - data := list.New[int](). - Append(5). - Append(4). - Append(7). - Append(7). - Append(2). - Append(1). - Filter(list.Is(7)) - - if data.Len() != 2 { - t.Errorf("Expected 2, got %d", data.Len()) - } -} - -func Test_List_Contains(t *testing.T) { - found := list.New[int](). - Append(5). - Append(4). - Append(7). - Append(7). - Append(2). - Append(1). - Contains(list.Is(2)) - - if !found { - t.Errorf("Expected to find 2") - } -} - -func Test_List_Contains_Fail(t *testing.T) { - found := list.New[int](). - Append(5). - Append(4). - Append(7). - Append(7). - Append(2). - Append(1). - Contains(list.Is(10)) - - if found { - t.Errorf("Expected to not find 10") - } -} - -func Test_Find(t *testing.T) { - ten, err := list.New[int](). - Append(5). - Append(4). - Append(7). - Append(7). - Append(2). - Append(10). - Find(list.Is(10)) - - if err != nil { - t.Errorf("Expected to find 10") - } - - if ten != 10 { - t.Errorf("Expected 10, got %d", ten) - } -} - -func Test_Find_Fail(t *testing.T) { - _, err := list.New[int](). - Append(5). - Append(4). - Append(7). - Append(7). - Append(2). - Append(10). - Find(list.Is(11)) - - if err == nil { - t.Errorf("Expected to not find 11") - } -} - -func Test_Distinct(t *testing.T) { - items := list.New[int](). - Append(4). - Append(7). - Append(7). - Append(7). - Append(10). - Append(10) - - expected := list.New[int](). - Append(4). - Append(7). - Append(10) - - if !items.Distinct(list.Eq[int]()).Equals(expected, list.Eq[int]()) { - t.Errorf("Expected %v, got %v", expected, items) - } -} diff --git a/server/types/list/sort.go b/server/types/list/sort.go deleted file mode 100644 index 4d6959ee..00000000 --- a/server/types/list/sort.go +++ /dev/null @@ -1,60 +0,0 @@ -package list - -// ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string - -type Mapper[T any, E any] func(T) E -type Filterer[T any] func(T) bool - -func Asc[T Ordered]() ComparisonFunc[T] { - return func(a, b T) bool { - return a < b - } -} - -func Eq[T Ordered]() ComparisonFunc[T] { - return func(a, b T) bool { - return a == b - } -} - -func Desc[T Ordered]() ComparisonFunc[T] { - return func(a, b T) bool { - return a > b - } -} - -func AscBy[T any, E Ordered](mapper Mapper[T, E]) ComparisonFunc[T] { - return func(a, b T) bool { - return mapper(a) < mapper(b) - } -} - -func DescBy[T any, E Ordered](mapper Mapper[T, E]) ComparisonFunc[T] { - return func(a, b T) bool { - return mapper(a) > mapper(b) - } -} - -func Is[T Ordered](value T) Filterer[T] { - return func(a T) bool { - return a == value - } -} - -func Not[T Ordered](value T) Filterer[T] { - return func(a T) bool { - return a != value - } -} - -func IsBy[T any, E Ordered](value E, mapper Mapper[T, E]) Filterer[T] { - return func(a T) bool { - return mapper(a) == value - } -} - -func NotBy[T any, E Ordered](value E, mapper Mapper[T, E]) Filterer[T] { - return func(a T) bool { - return mapper(a) != value - } -} diff --git a/server/types/pac/pacstore/store.go b/server/types/pac/pacstore/store.go index 63d63774..c2947ea1 100644 --- a/server/types/pac/pacstore/store.go +++ b/server/types/pac/pacstore/store.go @@ -3,42 +3,34 @@ package pacstore import ( "time" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" ) -type PacscriptList struct { - list.List[*pac.Script] -} - var lastModified time.Time -var loadedPacscripts PacscriptList +var loadedPacscripts []*pac.Script -func (l PacscriptList) FindByName(name string) (*pac.Script, error) { - return l.FindBy(func(pi *pac.Script) bool { - return pi.Name == name +func FindByName(name string) (*pac.Script, error) { + return array.FindBy(loadedPacscripts, func(p *pac.Script) bool { + return p.Name == name }) } -func (l PacscriptList) FindByMaintainer(maintainer string) (*pac.Script, error) { - return l.FindBy(func(pi *pac.Script) bool { - return pi.Maintainer == maintainer +func FindByMaintainer(maintainer string) (*pac.Script, error) { + return array.FindBy(loadedPacscripts, func(p *pac.Script) bool { + return p.Maintainer == maintainer }) } -func GetAll() PacscriptList { - return PacscriptList{ - loadedPacscripts.Clone().ToSlice(), - } +func GetAll() []*pac.Script { + return array.Clone(loadedPacscripts) } func LastModified() time.Time { return lastModified } -func Update(scripts list.List[*pac.Script]) { +func Update(scripts []*pac.Script) { lastModified = time.Now() - loadedPacscripts = PacscriptList{ - scripts.Clone(), - } + loadedPacscripts = scripts } diff --git a/server/types/pac/parser/git/lib.go b/server/types/pac/parser/git/lib.go index 757f4490..56d2df55 100644 --- a/server/types/pac/parser/git/lib.go +++ b/server/types/pac/parser/git/lib.go @@ -3,6 +3,8 @@ package git import ( "os" "os/exec" + + "github.com/joomcode/errorx" ) func hardResetAndPull(path string) error { @@ -13,7 +15,7 @@ func hardResetAndPull(path string) error { } // https://stackoverflow.com/a/41081908/13449010 - cmd = exec.Command("git", "fetch", "--depth=1") + cmd = exec.Command("git", "fetch") cmd.Dir = path if err := cmd.Run(); err != nil { return err @@ -35,9 +37,9 @@ func hardResetAndPull(path string) error { } func clonePrograms(path, url string) error { - cmd := exec.Command("git", "clone", "--depth=1", url, path) + cmd := exec.Command("git", "clone", url, path) if err := cmd.Run(); err != nil { - return err + return errorx.Decorate(err, "failed to run git clone command") } return nil @@ -49,11 +51,11 @@ func RefreshPrograms(path, url string) error { } if err := os.RemoveAll(path); err != nil { - return err + return errorx.Decorate(err, "failed to remove directory '%v'", path) } if err := clonePrograms(path, url); err != nil { - return err + return errorx.Decorate(err, "failed to clone repository '%v'", url) } return nil diff --git a/server/types/pac/parser/last_updated.go b/server/types/pac/parser/last_updated.go new file mode 100644 index 00000000..026a3ea9 --- /dev/null +++ b/server/types/pac/parser/last_updated.go @@ -0,0 +1,104 @@ +package parser + +import ( + "fmt" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/joomcode/errorx" + "pacstall.dev/webserver/config" + "pacstall.dev/webserver/consts" + "pacstall.dev/webserver/types/array" + "pacstall.dev/webserver/types/pac" + "pacstall.dev/webserver/types/pac/parser/pacsh" +) + +type packageLastUpdatedTuple struct { + packageName string + lastUpdated time.Time +} + +func getPackageLastUpdatedTuples() ([]packageLastUpdatedTuple, error) { + wordingDirectoryAbsolute, err := os.Getwd() + if err != nil { + return nil, errorx.Decorate(err, "failed to get absolute path to wording directory") + } + + programsPath := path.Join(wordingDirectoryAbsolute, config.GitClonePath) + script := fmt.Sprintf(` + cd %v + for i in ./packages/*/*.%s; do echo $i; git log -1 --pretty=\"%%at\" $i; done + `, programsPath, consts.PACSCRIPT_FILE_EXTENSION) + + outputBytes, err := pacsh.ExecBash(programsPath, "last_updated.sh", []byte(script)) + if err != nil { + return nil, errorx.Decorate(err, "failed to get last updated git output") + } + + output := string(outputBytes) + lines := strings.Split(output, "\n") + lines = lines[:len(lines)-1] // Remove last empty line + tuples := make([]packageLastUpdatedTuple, 0) + + for i := 0; i < len(lines)-1; i += 2 { + packagePath := lines[i] + lastUpdatedString := lines[i+1] // Unix time + + // Remove quotes + lastUpdatedString = lastUpdatedString[1 : len(lastUpdatedString) - 1] + + packageNameWithExtension := path.Base(packagePath) + packageName := strings.TrimSuffix(packageNameWithExtension, "."+consts.PACSCRIPT_FILE_EXTENSION) + + if packageName == "" || strings.HasPrefix(packageName, "-") { + return nil, errorx.IllegalState.New("failed to parse package name from package path '%v'", packagePath) + } + + lastUpdatedUnixTime, err := strconv.ParseInt(lastUpdatedString, 10, 64) + if err != nil { + return nil, errorx.Decorate(err, "failed to parse '%v' as int64", lastUpdatedString) + } + + lastUpdated := time.Unix(lastUpdatedUnixTime, 0).UTC() + + if lastUpdated.Year() < 2000 { + return nil, errorx.IllegalState.New("failed to parse last updated time for package '%v'. Given date is %v", packagePath, lastUpdatedString) + } + + tuples = append(tuples, packageLastUpdatedTuple{ + packageName: packageName, + lastUpdated: lastUpdated, + }) + } + + return tuples, nil +} + +func setLastUpdatedAt(packages []*pac.Script) error { + lastUpdatedTuples, err := getPackageLastUpdatedTuples() + if err != nil { + return errorx.Decorate(err, "failed to get package last updated tuples") + } + + packages = array.Clone(packages) + packages = array.SortBy(packages, func(s1, s2 *pac.Script) bool { + return s1.Name < s2.Name + }) + + lastUpdatedTuples = array.SortBy(lastUpdatedTuples, func(t1, t2 packageLastUpdatedTuple) bool { + return t1.packageName < t2.packageName + }) + + if len(lastUpdatedTuples) != len(packages) { + return errorx.AssertionFailed.New("expected %v package last updated tuples but got %v", len(packages), len(lastUpdatedTuples)) + } + + for i := 0; i < len(packages); i++ { + packages[i].LastUpdatedAt = lastUpdatedTuples[i].lastUpdated + } + + return nil +} diff --git a/server/types/pac/parser/pacscript.go b/server/types/pac/parser/pacscript.go index 7bae5671..2c96dff6 100644 --- a/server/types/pac/parser/pacscript.go +++ b/server/types/pac/parser/pacscript.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" "pacstall.dev/webserver/types/pac/parser/pacsh" ) @@ -62,17 +62,16 @@ fi return []byte(script) } -func computeRequiredBy(script pac.Script, scripts list.List[*pac.Script]) *pac.Script { - pickBeforeColon := func(line string) string { - return strings.Split(line, ": ")[0] +func computeRequiredBy(script *pac.Script, scripts []*pac.Script) { + pickBeforeColon := func(it *array.Iterator[string]) string { + return strings.Split(it.Value, ": ")[0] } - script.RequiredBy = list.Map( - scripts.Filter(func(s *pac.Script) bool { - return list.From(s.PacstallDependencies).Map(pickBeforeColon).Contains(list.Is(script.Name)) - }), func(_ int, s *pac.Script) string { - return s.Name - }) - - return &script + script.RequiredBy = make([]string, 0) + for _, otherScript := range scripts { + otherScriptDependencies := array.Map(otherScript.PacstallDependencies, pickBeforeColon) + if array.Contains(otherScriptDependencies, array.Is(script.Name)) { + script.RequiredBy = append(script.RequiredBy, otherScript.Name) + } + } } diff --git a/server/types/pac/parser/pacsh/exec_sh.go b/server/types/pac/parser/pacsh/exec_sh.go index 893d4670..8731c9c0 100644 --- a/server/types/pac/parser/pacsh/exec_sh.go +++ b/server/types/pac/parser/pacsh/exec_sh.go @@ -3,16 +3,17 @@ package pacsh import ( "os" + "github.com/joomcode/errorx" "pacstall.dev/webserver/log" ) var removeFile = os.Remove var ExecBash = execBash -func execBash(cwd, filename string, pacscript []byte) (stdout []byte, err error) { - tmpPath, err := CreateTempExecutable(cwd, filename, pacscript) +func execBash(cwd, filename string, content []byte) (stdout []byte, err error) { + tmpPath, err := CreateTempExecutable(cwd, filename, content) if err != nil { - return + return nil, errorx.Decorate(err, "failed to create temp executable") } defer removeFile(tmpPath) @@ -20,7 +21,7 @@ func execBash(cwd, filename string, pacscript []byte) (stdout []byte, err error) if err != nil { bytes, _ := os.ReadFile(tmpPath) log.Debug("Failed to execute '%v'. %v\n%v", tmpPath, err, string(bytes)) - return + return nil, errorx.Decorate(err, "failed to execute '%v'", tmpPath) } return stdout, nil diff --git a/server/types/pac/parser/pacsh/parse_pac_output.go b/server/types/pac/parser/pacsh/parse_pac_output.go index 9cdaf7d1..47e09e01 100644 --- a/server/types/pac/parser/pacsh/parse_pac_output.go +++ b/server/types/pac/parser/pacsh/parse_pac_output.go @@ -1,10 +1,10 @@ package pacsh import ( - "fmt" "strings" - "pacstall.dev/webserver/types/list" + "github.com/joomcode/errorx" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" ) @@ -34,15 +34,20 @@ const ( ) func parseSubcategory(category string) []string { - return list.From(strings.Split(category, "+ +++")).Map(func(s string) string { - return strings.TrimSpace(s) - }).Filter(list.Not("")) + subcategories := strings.Split(category, "+ +++") + for i, subcategory := range subcategories { + subcategories[i] = strings.TrimSpace(subcategory) + } + + return array.Filter(subcategories, func(it *array.Iterator[string]) bool { + return len(it.Value) > 0 + }) } func parseOutput(data []byte) (out pac.Script, err error) { content := string(data) - categories := list.From(strings.Split(content, "++++")).Map(func(s string) string { return strings.TrimSpace(s) }).ToSlice()[1:] + categories := array.Map(strings.Split(content, "++++"), func(it *array.Iterator[string]) string { return strings.TrimSpace(it.Value) })[1:] name := categories[nameIdx] packageName := categories[pkgnameIdx] maintainer := categories[maintainerIdx] @@ -70,7 +75,7 @@ func parseOutput(data []byte) (out pac.Script, err error) { if strings.HasSuffix(name, "-git") { version = "git" } else { - return out, fmt.Errorf("version is empty") + return out, errorx.IllegalArgument.New("expected version to be non-empty but got: %v", version) } } diff --git a/server/types/pac/parser/pacsh/pretty-name.go b/server/types/pac/parser/pacsh/pretty-name.go index 54edd27b..216df132 100644 --- a/server/types/pac/parser/pacsh/pretty-name.go +++ b/server/types/pac/parser/pacsh/pretty-name.go @@ -3,7 +3,6 @@ package pacsh import ( "strings" - "pacstall.dev/webserver/types/list" "pacstall.dev/webserver/types/pac" ) @@ -31,12 +30,14 @@ func getPrettyName(p pac.Script) string { } func titleCase(s string) string { - out := list.Reduce(strings.Split(s, "-"), func(word string, acc string) string { - if acc != "" { - acc += " " + title := "" + for _, word := range strings.Split(s, "-") { + if title != "" { + title += " " } - return acc + strings.ToUpper(word[:1]) + strings.ToLower(word[1:]) - }, "") - return out + title += strings.ToUpper(word[:1]) + strings.ToLower(word[1:]) + } + + return title } diff --git a/server/types/pac/parser/pacsh/temp_dir.go b/server/types/pac/parser/pacsh/temp_dir.go index 1a8f81d6..23ebc399 100644 --- a/server/types/pac/parser/pacsh/temp_dir.go +++ b/server/types/pac/parser/pacsh/temp_dir.go @@ -4,7 +4,7 @@ import ( "io/fs" "os" - "pacstall.dev/webserver/log" + "github.com/joomcode/errorx" ) var CreateTempDirectory = createTempDirectory @@ -16,14 +16,12 @@ var makeDir = os.Mkdir func createTempDirectory(path string) error { if _, err := statFile(path); os.IsNotExist(err) { if err = makeDir(path, fs.FileMode(int(0777))); err != nil { - log.Error("Failed to create temp dir '%v'\n%v", path, err) - return err + return errorx.Decorate(err, "failed to create temp dir '%v'", path) } } else { if err := removeAll(path); err != nil { - log.Error("Failed to remove existing temp dir '%v'", path) - return err + return errorx.Decorate(err, "failed to remove existing temp dir '%v'", path) } return createTempDirectory(path) diff --git a/server/types/pac/parser/pacsh/temp_dir_test.go b/server/types/pac/parser/pacsh/temp_dir_test.go index 647d5bdd..ce00df49 100644 --- a/server/types/pac/parser/pacsh/temp_dir_test.go +++ b/server/types/pac/parser/pacsh/temp_dir_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" ) func cleanup() { @@ -62,7 +62,7 @@ func Test_CreateTempDirectory_NoExisting(t *testing.T) { } statFileCalled += 1 - name, _ := list.From(strings.Split(path, "/")).Last() + name, _ := array.Last(strings.Split(path, "/")) return testFileInfo{ name: name, size: 0, @@ -85,19 +85,19 @@ func Test_CreateTempDirectory_NoExisting(t *testing.T) { err := CreateTempDirectory("/tmp") if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Errorf("expected no error, got %v", err) } if removeDirCalled != 1 { - t.Error("Expected removeAll to be called 1 time but was called", removeDirCalled) + t.Error("expected removeAll to be called 1 time but was called", removeDirCalled) } if makeDirCalled != 1 { - t.Error("Expected makeDir to be called 1 time but was called", makeDirCalled) + t.Error("expected makeDir to be called 1 time but was called", makeDirCalled) } if statFileCalled != 2 { - t.Error("Expected statFile to be called 2 times but was called", statFileCalled) + t.Error("expected statFile to be called 2 times but was called", statFileCalled) } } @@ -115,7 +115,7 @@ func Test_CreateTempDirectory_AlreadyExisting(t *testing.T) { } statFileCalled += 1 - name, _ := list.From(strings.Split(path, "/")).Last() + name, _ := array.Last(strings.Split(path, "/")) return testFileInfo{ name: name, size: 0, @@ -138,18 +138,18 @@ func Test_CreateTempDirectory_AlreadyExisting(t *testing.T) { err := CreateTempDirectory("/tmp") if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Errorf("expected no error, got %v", err) } if removeDirCalled != 0 { - t.Error("Expected removeAll to be called 0 times but was called", removeDirCalled) + t.Error("expected removeAll to be called 0 times but was called", removeDirCalled) } if makeDirCalled != 1 { - t.Error("Expected makeDir to be called 1 time but was called", makeDirCalled) + t.Error("expected makeDir to be called 1 time but was called", makeDirCalled) } if statFileCalled != 1 { - t.Error("Expected statFile to be called 2 times but was called", statFileCalled) + t.Error("expected statFile to be called 2 times but was called", statFileCalled) } } diff --git a/server/types/pac/parser/pacsh/temp_exec.go b/server/types/pac/parser/pacsh/temp_exec.go index 0d186f64..b87028e6 100644 --- a/server/types/pac/parser/pacsh/temp_exec.go +++ b/server/types/pac/parser/pacsh/temp_exec.go @@ -6,6 +6,7 @@ import ( "os/exec" "path" + "github.com/joomcode/errorx" "pacstall.dev/webserver/log" ) @@ -19,8 +20,7 @@ func createTempExecutable(dirPath, fileName string, content []byte) (string, err tmpFile, err := createFile(joinPaths(dirPath, fileName)) if err != nil { - log.Error("Failed to create temporary file '%v' in dir '%v'", fileName, dirPath) - return "", err + return "", errorx.Decorate(err, "failed to create temporary file '%v' in dir '%v'", fileName, dirPath) } defer tmpFile.Close() tmpPath := tmpFile.Name() @@ -29,18 +29,16 @@ func createTempExecutable(dirPath, fileName string, content []byte) (string, err cmd := execCommand("chmod", "+rwx", fileName) cmd.Dir = dirPath if err := cmd.Run(); err != nil { - log.Error("Failed to chmod temporary file '%v' in dir '%v'", fileName, dirPath) + log.Error("%+v", errorx.Decorate(err, "failed to chmod temporary file '%v' in dir '%v'", fileName, dirPath)) } }() if _, err = tmpFile.Write([]byte(content)); err != nil { - log.Error("Failed to write to file '%v'\n%v", tmpPath, err) - return "", err + return "", errorx.Decorate(err, "failed to write to file '%v'", tmpPath) } if err := tmpFile.Chmod(fs.FileMode(int(0777))); err != nil { - log.Error("Failed to chmod file '%v'\n%v", tmpPath, err) - return "", err + return "", errorx.Decorate(err, "failed to chmod file '%v'", tmpPath) } return tmpPath, nil diff --git a/server/types/pac/parser/parallelism/batch/run.go b/server/types/pac/parser/parallelism/batch/run.go index 834e9a54..95adb852 100644 --- a/server/types/pac/parser/parallelism/batch/run.go +++ b/server/types/pac/parser/parallelism/batch/run.go @@ -3,6 +3,8 @@ package batch import ( "sync/atomic" "time" + + "pacstall.dev/webserver/log" ) func Run[T any, E any](batchSize int, items []T, fn func(T) (E, error)) <-chan E { @@ -18,6 +20,8 @@ func Run[T any, E any](batchSize int, items []T, fn func(T) (E, error)) <-chan E if err == nil { out <- result + } else { + log.Error("batch error: %+v", err) } atomic.AddInt32(&left, -1) diff --git a/server/types/pac/parser/parallelism/batch/run_test.go b/server/types/pac/parser/parallelism/batch/run_test.go index 198e07d5..7268e397 100644 --- a/server/types/pac/parser/parallelism/batch/run_test.go +++ b/server/types/pac/parser/parallelism/batch/run_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac/parser/parallelism/batch" "pacstall.dev/webserver/types/pac/parser/parallelism/channels" ) @@ -22,7 +22,7 @@ func Test_Batch_Run(t *testing.T) { return item, nil }) - if channels.ToList(out).SortBy(list.Asc[int]()).Equals(items, list.Eq[int]()) == false { - t.Error("Expected results to be sorted") + if array.Equals(array.SortBy(channels.ToSlice(out), array.Asc[int]()), items, array.Eq[int]()) == false { + t.Error("expected results to be sorted") } } diff --git a/server/types/pac/parser/parallelism/channels/to_slice.go b/server/types/pac/parser/parallelism/channels/to_slice.go index 4e5a1f25..1f469dc7 100644 --- a/server/types/pac/parser/parallelism/channels/to_slice.go +++ b/server/types/pac/parser/parallelism/channels/to_slice.go @@ -1,7 +1,5 @@ package channels -import "pacstall.dev/webserver/types/list" - func ToSlice[T any](in <-chan T) []T { out := make([]T, 0) for item := range in { @@ -10,7 +8,3 @@ func ToSlice[T any](in <-chan T) []T { return out } - -func ToList[T any](in <-chan T) list.List[T] { - return list.From(ToSlice(in)) -} diff --git a/server/types/pac/parser/parse.go b/server/types/pac/parser/parse.go index d7e83cbe..18028437 100644 --- a/server/types/pac/parser/parse.go +++ b/server/types/pac/parser/parse.go @@ -6,11 +6,13 @@ import ( "path" "strings" + "github.com/joomcode/errorx" "pacstall.dev/webserver/config" + "pacstall.dev/webserver/consts" "pacstall.dev/webserver/log" "pacstall.dev/webserver/repology" "pacstall.dev/webserver/types" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" "pacstall.dev/webserver/types/pac/pacstore" "pacstall.dev/webserver/types/pac/parser/git" @@ -23,28 +25,38 @@ const PACKAGE_LIST_FILE_NAME = "./packagelist" func ParseAll() error { if err := git.RefreshPrograms(config.GitClonePath, config.GitURL); err != nil { - return fmt.Errorf("could not update repository 'pacstall-programs'. %v", err) + return errorx.Decorate(err, "could not update repository 'pacstall-programs'") } pkgList, err := readKnownPacscriptNames() if err != nil { - return fmt.Errorf("failed to parse packagelist. %v", err) + return errorx.Decorate(err, "failed to parse packagelist") } - loadedPacscripts := list.From(parsePacscriptFiles(pkgList)).MapExt(func(p *pac.Script, scripts list.List[*pac.Script]) *pac.Script { - return computeRequiredBy(*p, scripts) - }).SortBy(func(s1, s2 *pac.Script) bool { + loadedPacscripts, err := parsePacscriptFiles(pkgList) + if err != nil { + return errorx.Decorate(err, "failed to parse pacscripts") + } + + for _, script := range loadedPacscripts { + computeRequiredBy(script, loadedPacscripts) + } + + array.SortBy(loadedPacscripts, func(s1, s2 *pac.Script) bool { return s1.Name < s2.Name }) + if err := setLastUpdatedAt(loadedPacscripts); err != nil { + return errorx.Decorate(err, "failed to set last updated at") + } + pacstore.Update(loadedPacscripts) - log.Info("Successfully parsed %v (%v / %v) packages", types.Percent(float64(len(loadedPacscripts))/float64(pkgList.Len())), loadedPacscripts.Len(), pkgList.Len()) - log.Notify("Successfully parsed %v (%v / %v) packages", types.Percent(float64(len(loadedPacscripts))/float64(pkgList.Len())), loadedPacscripts.Len(), pkgList.Len()) + log.Info("successfully parsed %v (%v / %v) packages", types.Percent(float64(len(loadedPacscripts))/float64(len(pkgList))), len(loadedPacscripts), len(pkgList)) return nil } -func readKnownPacscriptNames() (list.List[string], error) { +func readKnownPacscriptNames() ([]string, error) { pkglistPath := path.Join(config.GitClonePath, PACKAGE_LIST_FILE_NAME) bytes, err := os.ReadFile(pkglistPath) if err != nil { @@ -59,39 +71,37 @@ func readKnownPacscriptNames() (list.List[string], error) { return names, nil } -func parsePacscriptFiles(names []string) []*pac.Script { +func parsePacscriptFiles(names []string) ([]*pac.Script, error) { if err := pacsh.CreateTempDirectory(config.TempDir); err != nil { - log.Error("Failed to create temporary directory. %v", err) - return nil + return nil, errorx.Decorate(err, "failed to create temporary directory") } - log.Info("Parsing pacscripts...") + log.Info("parsing pacscripts...") outChan := batch.Run(int(config.MaxOpenFiles), names, func(pacName string) (*pac.Script, error) { out, err := ParsePacscriptFile(config.GitClonePath, pacName) if err != nil { - log.Warn("Failed to parse %v. err: %v", pacName, err) + log.Warn("failed to parse %v. err: %v", pacName, err) } if config.Repology.Enabled { if err := repology.Sync(&out); err != nil { - log.Debug("Failed to sync %v with repology. Error: %v", pacName, err) + log.Debug("failed to sync %v with repology. Error: %v", pacName, err) } } return &out, err }) - return channels.ToSlice(outChan) + return channels.ToSlice(outChan), nil } func readPacscriptFile(rootDir, name string) (scriptBytes []byte, fileName string, err error) { - fileName = fmt.Sprintf("%v.pacscript", name) + fileName = fmt.Sprintf("%s.%s", name, consts.PACSCRIPT_FILE_EXTENSION) scriptPath := path.Join(rootDir, "packages", name, fileName) scriptBytes, err = os.ReadFile(scriptPath) if err != nil { - log.Error("Failed to read package pacsh '%v'\n%v", scriptPath, err) - return + return nil, "", errorx.Decorate(err, "failed to read file '%v'", scriptPath) } return scriptBytes, fileName, nil @@ -100,19 +110,19 @@ func readPacscriptFile(rootDir, name string) (scriptBytes []byte, fileName strin func ParsePacscriptFile(programsDirPath, name string) (pac.Script, error) { pacshell, filename, err := readPacscriptFile(programsDirPath, name) if err != nil { - return pac.Script{}, err + return pac.Script{}, errorx.Decorate(err, "failed to read pacscript '%v'", name) } pacshell = buildCustomFormatScript(pacshell) stdout, err := pacsh.ExecBash(config.TempDir, filename, pacshell) if err != nil { - return pac.Script{}, err + return pac.Script{}, errorx.Decorate(err, "failed to execute pacscript '%v'", name) } pacscript, err := pacsh.ParsePacOutput(stdout) if err != nil { - return pac.Script{}, fmt.Errorf("failed to parse pacscript %v. err: %v", name, err) + return pac.Script{}, errorx.Decorate(err, "failed to parse pacscript '%v'", name) } return pacscript, nil diff --git a/server/types/pac/parser/scheduler.go b/server/types/pac/parser/scheduler.go index 9700692d..9edd7925 100644 --- a/server/types/pac/parser/scheduler.go +++ b/server/types/pac/parser/scheduler.go @@ -7,14 +7,21 @@ import ( ) func ScheduleRefresh(every time.Duration) { - go func() { - for { - err := ParseAll() - if err != nil { - log.Error("Failed to parse pacscripts: %v", err) - } + go refresh(every) +} + +func refresh(every time.Duration) { + for { + err := ParseAll() + if err != nil { + log.Error("parse error: %+v", err) - time.Sleep(every) + retryIn := time.Second * 30 + log.Info("retrying in %v", retryIn) + time.Sleep(retryIn) + continue } - }() + + time.Sleep(every) + } } diff --git a/server/types/pac/parser/search.go b/server/types/pac/parser/search.go index b06dab52..179091af 100644 --- a/server/types/pac/parser/search.go +++ b/server/types/pac/parser/search.go @@ -3,7 +3,7 @@ package parser import ( "strings" - "pacstall.dev/webserver/types/list" + "pacstall.dev/webserver/types/array" "pacstall.dev/webserver/types/pac" ) @@ -56,38 +56,38 @@ func SortPackages(packages []*pac.Script, sortType, sortBy string) []*pac.Script return packages } - out := list.From(packages) + out := array.Clone(packages) switch sortBy { case "name": if strings.Compare(sortType, "asc") == 0 { - out = out.SortBy(func(a, b *pac.Script) bool { + out = array.SortBy(out, func(a, b *pac.Script) bool { return strings.Compare(a.Name, b.Name) < 0 }) } else { - out = out.SortBy(func(a, b *pac.Script) bool { + out = array.SortBy(out, func(a, b *pac.Script) bool { return strings.Compare(a.Name, b.Name) > 0 }) } case "maintainer": if strings.Compare(sortType, "asc") == 0 { - out = out.SortBy(func(a, b *pac.Script) bool { + out = array.SortBy(out, func(a, b *pac.Script) bool { return strings.Compare(a.Maintainer, b.Maintainer) < 0 }) } else { - out = out.SortBy(func(a, b *pac.Script) bool { + out = array.SortBy(out, func(a, b *pac.Script) bool { return strings.Compare(a.Maintainer, b.Maintainer) > 0 }) } case "version": if strings.Compare(sortType, "asc") == 0 { - out = out.SortBy(func(a, b *pac.Script) bool { + out = array.SortBy(out, func(a, b *pac.Script) bool { return strings.Compare(a.Version, b.Version) < 0 }) } else { - out = out.SortBy(func(a, b *pac.Script) bool { + out = array.SortBy(out, func(a, b *pac.Script) bool { return strings.Compare(a.Version, b.Version) > 0 }) } diff --git a/server/types/pac/script.go b/server/types/pac/script.go index 7e233014..50f93d75 100644 --- a/server/types/pac/script.go +++ b/server/types/pac/script.go @@ -1,5 +1,7 @@ package pac +import "time" + type updateStatus struct { Unknown UpdateStatusValue Latest UpdateStatusValue @@ -19,25 +21,26 @@ var UpdateStatus = updateStatus{ type UpdateStatusValue = int type Script struct { - Name string `json:"name"` - PrettyName string `json:"prettyName"` - Version string `json:"version"` - LatestVersion *string `json:"latestVersion"` - PackageName string `json:"packageName"` - Maintainer string `json:"maintainer"` - Description string `json:"description"` - URL string `json:"url"` - RuntimeDependencies []string `json:"runtimeDependencies"` - BuildDependencies []string `json:"buildDependencies"` - OptionalDependencies []string `json:"optionalDependencies"` - Breaks []string `json:"breaks"` - Gives string `json:"gives"` - Replace []string `json:"replace"` - Hash *string `json:"hash"` - PPA []string `json:"ppa"` - PacstallDependencies []string `json:"pacstallDependencies"` - Patch []string `json:"patch"` - Repology []string `json:"repology"` - RequiredBy []string `json:"requiredBy"` - UpdateStatus int `json:"updateStatus"` // enum UpdateStatus + Name string `json:"name"` + PrettyName string `json:"prettyName"` + Version string `json:"version"` + LatestVersion *string `json:"latestVersion"` + PackageName string `json:"packageName"` + Maintainer string `json:"maintainer"` + Description string `json:"description"` + URL string `json:"url"` + RuntimeDependencies []string `json:"runtimeDependencies"` + BuildDependencies []string `json:"buildDependencies"` + OptionalDependencies []string `json:"optionalDependencies"` + Breaks []string `json:"breaks"` + Gives string `json:"gives"` + Replace []string `json:"replace"` + Hash *string `json:"hash"` + PPA []string `json:"ppa"` + PacstallDependencies []string `json:"pacstallDependencies"` + Patch []string `json:"patch"` + Repology []string `json:"repology"` + RequiredBy []string `json:"requiredBy"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + UpdateStatus int `json:"updateStatus"` // enum UpdateStatus }