This commit is contained in:
2025-12-07 14:54:35 +01:00
parent ac6a4fd6d3
commit d18a72d492
18 changed files with 725 additions and 307 deletions
+21 -18
View File
@@ -5,36 +5,39 @@ import (
)
type Domain struct {
SoftwareRepository SoftwareRepository
ReleaseRepository ReleaseRepository
DownloadService DownloadService
SoftwareUpdaterService SoftwareUpdaterService
SoftwareService SoftwareService
SoftwareRepository SoftwareRepositoryInterface
ReleaseRepository ReleaseRepositoryInterface
FileRepository FileRepositoryInterface
DownloadService DownloadServiceInterface
SoftwareUpdaterService SoftwareUpdaterServiceInterface
SoftwareService SoftwareServiceInterface
}
func NewDomain() Domain {
DB := mysql_utils.Init()
MigrateGoDatabase(DB)
softwareRepository := &softwareRepository{db: DB}
releaseRepository := &releaseRepository{db: DB}
softwareService := &softwareService{softwareRepository: softwareRepository}
tic80Updater := &softwareUpdaterTIC80Service{
softwareRepository: softwareRepository,
releaseRepository: releaseRepository,
}
softwareRepository := &SoftwareRepository{db: DB}
releaseRepository := &ReleaseRepository{db: DB}
fileRepository := NewFileRepository()
softwareUpdaterService := &softwareUpdaterService{tic80Updater: tic80Updater}
softwareService := NewSoftwareService(softwareRepository)
tic80Updater := NewSoftwareUpdaterTIC80Service(
softwareRepository,
releaseRepository,
fileRepository,
)
downloadServiceInstance := &downloadService{
softwareRepository: softwareRepository,
releaseRepository: releaseRepository,
}
softwareUpdaterService := NewSoftwareUpdaterService(tic80Updater)
downloadService := NewDownloadService(softwareRepository, releaseRepository)
return Domain{
SoftwareRepository: softwareRepository,
ReleaseRepository: releaseRepository,
DownloadService: downloadServiceInstance,
FileRepository: fileRepository,
DownloadService: downloadService,
SoftwareUpdaterService: softwareUpdaterService,
SoftwareService: softwareService,
}
-1
View File
@@ -4,7 +4,6 @@ import "gorm.io/gorm"
type Release struct {
gorm.Model
ID uint `gorm:"primaryKey" json:"id"`
SoftwareID uint `gorm:"index" json:"software_id"`
Version string `gorm:"size:64" json:"version"`
CartridgePath string `gorm:"size:255" json:"-"`
-1
View File
@@ -4,7 +4,6 @@ import "gorm.io/gorm"
type Software struct {
gorm.Model
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;size:128" json:"name"`
Title string `gorm:"size:255" json:"title"`
Author string `gorm:"size:255" json:"author"`
+152
View File
@@ -0,0 +1,152 @@
package domain
import (
"archive/zip"
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
type FileRepositoryInterface interface {
FileExists(fileName, basePath string) bool
CreateDir(dirPath string) error
DeleteFile(fileName, basePath string) error
MoveFile(srcFileName, destPath, basePath string) error
UnzipHTMLContent(zipFileName, baseName, basePath string) error
GetSoftwareDir(softwareName, basePath string) string
GetCartridgePath(softwareName, version, basePath string) string
GetSourcePath(softwareName, version, basePath string) string
ReadMetaFromFile(fileName, basePath string) (map[string]string, error)
}
type FileRepository struct{}
func NewFileRepository() *FileRepository {
return &FileRepository{}
}
func (r *FileRepository) FileExists(fileName, basePath string) bool {
filePath := filepath.Join(basePath, fileName)
_, err := os.Stat(filePath)
return err == nil
}
func (r *FileRepository) CreateDir(dirPath string) error {
return os.MkdirAll(dirPath, os.ModePerm)
}
func (r *FileRepository) DeleteFile(fileName, basePath string) error {
filePath := filepath.Join(basePath, fileName)
return os.Remove(filePath)
}
func (r *FileRepository) MoveFile(srcFileName, destPath, basePath string) error {
srcPath := filepath.Join(basePath, srcFileName)
return os.Rename(srcPath, destPath)
}
func (r *FileRepository) UnzipHTMLContent(zipFileName, baseName, basePath string) error {
zipFilePath := filepath.Join(basePath, zipFileName)
destDir := filepath.Join(basePath, "html", baseName)
fmt.Printf("FileRepository: Unzipping %s to %s\n", zipFilePath, destDir)
reader, err := zip.OpenReader(zipFilePath)
if err != nil {
return err
}
defer reader.Close()
destDir, err = filepath.Abs(destDir)
if err != nil {
return err
}
for _, f := range reader.File {
fpath := filepath.Join(destDir, f.Name)
if !strings.HasPrefix(fpath, destDir) {
return fmt.Errorf("%s: illegal file path", fpath)
}
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
outFile.Close()
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
return nil
}
func (r *FileRepository) GetSoftwareDir(softwareName, basePath string) string {
return filepath.Join(basePath, softwareName)
}
func (r *FileRepository) GetCartridgePath(softwareName, version, basePath string) string {
softwareDir := r.GetSoftwareDir(softwareName, basePath)
return filepath.Join(softwareDir, fmt.Sprintf("%s-%s.tic", softwareName, version))
}
func (r *FileRepository) GetSourcePath(softwareName, version, basePath string) string {
softwareDir := r.GetSoftwareDir(softwareName, basePath)
return filepath.Join(softwareDir, fmt.Sprintf("%s-%s.lua", softwareName, version))
}
func (r *FileRepository) ReadMetaFromFile(fileName, basePath string) (map[string]string, error) {
filePath := filepath.Join(basePath, fileName)
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
metaData := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "--") {
parts := strings.SplitN(strings.TrimPrefix(line, "--"), ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
metaData[strings.ToLower(key)] = val
} else {
break
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return metaData, nil
}
+5 -10
View File
@@ -2,32 +2,27 @@ package domain
import "gorm.io/gorm"
type ReleaseRepository interface {
type ReleaseRepositoryInterface interface {
Create(release *Release) error
FindLatestBySoftwareID(softwareID uint) (*Release, error)
FindBySoftwareIDAndVersion(softwareID uint, version string) (*Release, error)
}
type releaseRepository struct {
type ReleaseRepository struct {
db *gorm.DB
}
// NewReleaseRepository creates a new instance of ReleaseRepository.
/* func NewReleaseRepository(db *gorm.DB) ReleaseRepository {
return &releaseRepository{db}
} */
func (r *releaseRepository) Create(release *Release) error {
func (r *ReleaseRepository) Create(release *Release) error {
return r.db.Create(release).Error
}
func (r *releaseRepository) FindLatestBySoftwareID(softwareID uint) (*Release, error) {
func (r *ReleaseRepository) FindLatestBySoftwareID(softwareID uint) (*Release, error) {
var release Release
err := r.db.Where("software_id = ?", softwareID).Order("created_at desc").First(&release).Error
return &release, err
}
func (r *releaseRepository) FindBySoftwareIDAndVersion(softwareID uint, version string) (*Release, error) {
func (r *ReleaseRepository) FindBySoftwareIDAndVersion(softwareID uint, version string) (*Release, error) {
var release Release
err := r.db.Where("software_id = ? AND version = ?", softwareID, version).First(&release).Error
return &release, err
+5 -5
View File
@@ -2,17 +2,17 @@ package domain
import "gorm.io/gorm"
type SoftwareRepository interface {
type SoftwareRepositoryInterface interface {
List() ([]Software, error)
GetByName(name string) (*Software, error)
UpdateOrCreate(software *Software) error
}
type softwareRepository struct {
type SoftwareRepository struct {
db *gorm.DB
}
func (r *softwareRepository) GetByName(name string) (*Software, error) {
func (r *SoftwareRepository) GetByName(name string) (*Software, error) {
var software Software
if err := r.db.Preload("Releases").Where("name = ?", name).First(&software).Error; err != nil {
return nil, err
@@ -20,7 +20,7 @@ func (r *softwareRepository) GetByName(name string) (*Software, error) {
return &software, nil
}
func (r *softwareRepository) List() ([]Software, error) {
func (r *SoftwareRepository) List() ([]Software, error) {
var softwares []Software
if err := r.db.Preload("Releases").Find(&softwares).Error; err != nil {
return nil, err
@@ -28,7 +28,7 @@ func (r *softwareRepository) List() ([]Software, error) {
return softwares, nil
}
func (r *softwareRepository) UpdateOrCreate(software *Software) error {
func (r *SoftwareRepository) UpdateOrCreate(software *Software) error {
var existing Software
if err := r.db.Where("name = ?", software.Name).First(&existing).Error; err == nil {
software.ID = existing.ID
+13 -6
View File
@@ -10,17 +10,24 @@ var (
ErrReleaseNotFound = errors.New("release not found for software and version")
)
type DownloadService interface {
type DownloadServiceInterface interface {
GetLatestRelease(softwareName string) (*Release, error)
GetSpecificRelease(softwareName string, version string) (*Release, error)
}
type downloadService struct {
softwareRepository SoftwareRepository
releaseRepository ReleaseRepository
type DownloadService struct {
softwareRepository SoftwareRepositoryInterface
releaseRepository ReleaseRepositoryInterface
}
func (s *downloadService) GetLatestRelease(softwareName string) (*Release, error) {
func NewDownloadService(softwareRepository SoftwareRepositoryInterface, releaseRepository ReleaseRepositoryInterface) *DownloadService {
return &DownloadService{
softwareRepository: softwareRepository,
releaseRepository: releaseRepository,
}
}
func (s *DownloadService) GetLatestRelease(softwareName string) (*Release, error) {
software, err := s.softwareRepository.GetByName(softwareName)
if err != nil {
return nil, ErrSoftwareNotFound
@@ -34,7 +41,7 @@ func (s *downloadService) GetLatestRelease(softwareName string) (*Release, error
return release, nil
}
func (s *downloadService) GetSpecificRelease(softwareName string, version string) (*Release, error) {
func (s *DownloadService) GetSpecificRelease(softwareName string, version string) (*Release, error) {
software, err := s.softwareRepository.GetByName(softwareName)
if err != nil {
return nil, ErrSoftwareNotFound
+11 -8
View File
@@ -7,17 +7,21 @@ type SoftwareListDTO struct {
LatestRelease *Release
}
type SoftwareService interface {
type SoftwareServiceInterface interface {
List() ([]SoftwareListDTO, error)
GetByNameWithReleases(name string) (*Software, error)
}
type softwareService struct {
softwareRepository SoftwareRepository
type SoftwareService struct {
repository SoftwareRepositoryInterface
}
func (s *softwareService) List() ([]SoftwareListDTO, error) {
softwares, err := s.softwareRepository.List()
func NewSoftwareService(repository SoftwareRepositoryInterface) *SoftwareService {
return &SoftwareService{repository: repository}
}
func (s *SoftwareService) List() ([]SoftwareListDTO, error) {
softwares, err := s.repository.List()
if err != nil {
return nil, err
}
@@ -26,7 +30,6 @@ func (s *softwareService) List() ([]SoftwareListDTO, error) {
for _, sw := range softwares {
dto := SoftwareListDTO{Software: sw}
if len(sw.Releases) > 0 {
// Sort releases by CreatedAt in descending order to find the latest
sort.Slice(sw.Releases, func(i, j int) bool {
return sw.Releases[i].CreatedAt.After(sw.Releases[j].CreatedAt)
})
@@ -38,6 +41,6 @@ func (s *softwareService) List() ([]SoftwareListDTO, error) {
return dtos, nil
}
func (s *softwareService) GetByNameWithReleases(name string) (*Software, error) {
return s.softwareRepository.GetByName(name)
func (s *SoftwareService) GetByNameWithReleases(name string) (*Software, error) {
return s.repository.GetByName(name)
}
+10 -8
View File
@@ -4,19 +4,21 @@ import (
"fmt"
)
// Keep os for getting GAME_PATH in TIC80 service if needed
type SoftwareUpdaterService interface {
UpdateSoftware(platform, name string) error
type SoftwareUpdaterServiceInterface interface {
Update(platform, name string) error
}
type softwareUpdaterService struct {
tic80Updater SoftwareUpdaterTIC80Service
type SoftwareUpdaterService struct {
tic80Updater SoftwareUpdaterTIC80ServiceInterface
}
func (s *softwareUpdaterService) UpdateSoftware(platform, name string) error {
func NewSoftwareUpdaterService(tic80Updater SoftwareUpdaterTIC80ServiceInterface) *SoftwareUpdaterService {
return &SoftwareUpdaterService{tic80Updater: tic80Updater}
}
func (s *SoftwareUpdaterService) Update(platform, name string) error {
if platform == "tic80" {
return s.tic80Updater.UpdateTIC80Software(name)
return s.tic80Updater.Update(name)
}
return fmt.Errorf("unsupported platform: %s", platform)
}
+126 -166
View File
@@ -1,193 +1,153 @@
package domain
import (
"archive/zip"
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
const TIC80_GAME_EXTENSION = ".tic" // Assuming .tic is the extension for TIC-80 games
type SoftwareUpdaterTIC80Service interface {
UpdateTIC80Software(name string) error
type SoftwareUpdaterTIC80ServiceInterface interface {
Update(name string) error
}
type softwareUpdaterTIC80Service struct {
softwareRepository SoftwareRepository
releaseRepository ReleaseRepository
type SoftwareUpdaterTIC80Service struct {
softwareRepository SoftwareRepositoryInterface
releaseRepository ReleaseRepositoryInterface
fileRepository FileRepositoryInterface
}
func (s *softwareUpdaterTIC80Service) UpdateTIC80Software(name string) error {
func NewSoftwareUpdaterTIC80Service(
softwareRepository SoftwareRepositoryInterface,
releaseRepository ReleaseRepositoryInterface,
fileRepository FileRepositoryInterface,
) *SoftwareUpdaterTIC80Service {
return &SoftwareUpdaterTIC80Service{
softwareRepository: softwareRepository,
releaseRepository: releaseRepository,
fileRepository: fileRepository,
}
}
func (s *SoftwareUpdaterTIC80Service) Update(name string) error {
fmt.Printf("TIC80 Updater: Starting update for name: %s\n", name)
gamePath, _ := os.LookupEnv("GAMES_DIR")
// Let's assume 'name' could be "mygame" which means "mygame.html.zip" or "mygame.lua" / "mygame.tic"
contentsPath, _ := os.LookupEnv("CONTENTS_DIR")
// Case 1: HTML game (name.html.zip)
zipFileName := fmt.Sprintf("%s.html.zip", name)
zipFilePath := filepath.Join(gamePath, zipFileName)
if _, err := os.Stat(zipFilePath); err == nil { // If the zip file exists
baseName := name
destDir := filepath.Join(gamePath, "html", baseName) // Destination is GAMES_DIR/html/name
fmt.Printf("TIC80 Updater: Unzipping %s to %s\n", zipFilePath, destDir)
if err := s.unzipSource(zipFilePath, destDir); err != nil {
return fmt.Errorf("error unzipping %s: %w", zipFileName, err)
}
if err := os.Remove(zipFilePath); err != nil {
fmt.Printf("TIC80 Updater: Deleting zip file: %s\n", zipFilePath)
return fmt.Errorf("error deleting zip file %s: %w", zipFileName, err)
}
fmt.Printf("TIC80 Updater: Successfully processed HTML game: %s\n", name)
// Case 1: HTML content (name.html.zip)
if err := s.handleHTMLContent(name, contentsPath); err == nil {
fmt.Printf("TIC80 Updater: Successfully processed HTML content: %s\n", name)
return nil
}
// Case 2: TIC-80 Lua game (name.lua and name.tic)
luaFileName := fmt.Sprintf("%s.lua", name)
cartridgeFileName := fmt.Sprintf("%s.tic", name)
luaFilePath := filepath.Join(gamePath, luaFileName)
cartridgeFilePath := filepath.Join(gamePath, cartridgeFileName)
if _, err := os.Stat(luaFilePath); err == nil { // If the lua file exists
if _, err := os.Stat(cartridgeFilePath); os.IsNotExist(err) {
return fmt.Errorf("missing cartridge file '%s' for '%s'", cartridgeFileName, luaFileName)
}
software, version := s.parseMeta(luaFilePath, name)
if version == "" {
return fmt.Errorf("missing version info in '%s'", luaFileName)
}
if err := s.softwareRepository.UpdateOrCreate(&software); err != nil {
return err
}
// Create software directory
softwareDir := filepath.Join(gamePath, software.Name)
fmt.Printf("TIC80 Updater: Creating software directory: %s\n", softwareDir)
os.MkdirAll(softwareDir, os.ModePerm)
// New file paths
newCartridgePath := filepath.Join(softwareDir, fmt.Sprintf("%s-%s.tic", software.Name, version))
newSourcePath := filepath.Join(softwareDir, fmt.Sprintf("%s-%s.lua", software.Name, version))
// Move files
fmt.Printf("TIC80 Updater: Moving cartridge from %s to %s\n", cartridgeFilePath, newCartridgePath)
if err := os.Rename(cartridgeFilePath, newCartridgePath); err != nil {
return err
}
fmt.Printf("TIC80 Updater: Moving Lua source from %s to %s\n", luaFilePath, newSourcePath)
if err := os.Rename(luaFilePath, newSourcePath); err != nil {
return err
}
// Create Release
release := &Release{
SoftwareID: software.ID,
Version: version,
CartridgePath: newCartridgePath,
SourcePath: newSourcePath,
}
if err := s.releaseRepository.Create(release); err != nil {
return err
}
fmt.Printf("TIC80 Updater: Successfully processed Lua game: %s, version: %s\n", name, version)
return nil // Lua game processed
}
return fmt.Errorf("no recognizable game file found for '%s' in '%s'", name, gamePath)
}
// unzipSource extracts a zip archive to a destination directory.
func (s *softwareUpdaterTIC80Service) unzipSource(source, destination string) error {
reader, err := zip.OpenReader(source)
if err != nil {
return err
}
defer reader.Close()
destination, err = filepath.Abs(destination)
if err != nil {
// Case 2: TIC-80 Lua cartridge (name.lua and name.tic)
if err := s.handleLuaCartridge(name, contentsPath); err != nil {
return err
}
for _, f := range reader.File {
fpath := filepath.Join(destination, f.Name)
if !strings.HasPrefix(fpath, destination) {
return fmt.Errorf("%s: illegal file path", fpath)
}
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
fmt.Printf("TIC80 Updater: Successfully processed Lua cartridge: %s\n", name)
return nil
}
func (s *softwareUpdaterTIC80Service) parseMeta(path string, name string) (Software, string) {
file, _ := os.Open(path)
defer file.Close()
func (s *SoftwareUpdaterTIC80Service) handleHTMLContent(name, contentsPath string) error {
zipFileName := fmt.Sprintf("%s.html.zip", name)
baseName := name
var g Software
if err := s.fileRepository.UnzipHTMLContent(zipFileName, baseName, contentsPath); err != nil {
return err
}
return s.fileRepository.DeleteFile(zipFileName, contentsPath)
}
func (s *SoftwareUpdaterTIC80Service) handleLuaCartridge(name, contentsPath string) error {
luaFileName := fmt.Sprintf("%s.lua", name)
cartridgeFileName := fmt.Sprintf("%s.tic", name)
if !s.fileRepository.FileExists(luaFileName, contentsPath) {
return fmt.Errorf("no recognizable content file found for '%s' in '%s'", name, contentsPath)
}
if !s.fileRepository.FileExists(cartridgeFileName, contentsPath) {
return fmt.Errorf("missing cartridge file '%s' for '%s'", cartridgeFileName, luaFileName)
}
software, version, err := s.parseMeta(luaFileName, name, contentsPath)
if err != nil {
return err
}
if version == "" {
return fmt.Errorf("missing version info in '%s'", luaFileName)
}
if err := s.softwareRepository.UpdateOrCreate(&software); err != nil {
return err
}
if err := s.moveCartridgeFiles(name, software, version, luaFileName, cartridgeFileName, contentsPath); err != nil {
return err
}
release := &Release{
SoftwareID: software.ID,
Version: version,
CartridgePath: s.fileRepository.GetCartridgePath(software.Name, version, contentsPath),
SourcePath: s.fileRepository.GetSourcePath(software.Name, version, contentsPath),
}
return s.releaseRepository.Create(release)
}
func (s *SoftwareUpdaterTIC80Service) moveCartridgeFiles(name string, software Software, version, luaFileName, cartridgeFileName, contentsPath string) error {
softwareDir := s.fileRepository.GetSoftwareDir(software.Name, contentsPath)
fmt.Printf("TIC80 Updater: Creating software directory: %s\n", softwareDir)
if err := s.fileRepository.CreateDir(softwareDir); err != nil {
return err
}
newCartridgePath := s.fileRepository.GetCartridgePath(software.Name, version, contentsPath)
newSourcePath := s.fileRepository.GetSourcePath(software.Name, version, contentsPath)
fmt.Printf("TIC80 Updater: Moving cartridge from %s to %s\n", cartridgeFileName, newCartridgePath)
if err := s.fileRepository.MoveFile(cartridgeFileName, newCartridgePath, contentsPath); err != nil {
return err
}
fmt.Printf("TIC80 Updater: Moving Lua source from %s to %s\n", luaFileName, newSourcePath)
if err := s.fileRepository.MoveFile(luaFileName, newSourcePath, contentsPath); err != nil {
return err
}
return nil
}
func (s *SoftwareUpdaterTIC80Service) parseMeta(luaFileName, name, contentsPath string) (Software, string, error) {
var software Software
var version string
g.Name = name
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "--") {
parts := strings.SplitN(strings.TrimPrefix(line, "--"), ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
switch strings.ToLower(key) {
case "title":
g.Title = val
case "author":
g.Author = val
case "desc":
g.Desc = val
case "site":
g.Site = val
case "license":
g.License = val
case "version":
version = val
}
} else {
break
metaData, err := s.fileRepository.ReadMetaFromFile(luaFileName, contentsPath)
if err != nil {
return software, "", err
}
software.Name = name
software.Platform = "tic80"
for key, val := range metaData {
switch key {
case "title":
software.Title = val
case "author":
software.Author = val
case "desc":
software.Desc = val
case "site":
software.Site = val
case "license":
software.License = val
case "version":
version = val
}
}
return g, version
return software, version, nil
}