initial commit

This commit is contained in:
2025-12-07 11:32:45 +01:00
commit c3daed67ac
35 changed files with 1315 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
package domain
import (
"teletype_softwares/lib/mysql_utils"
)
type Domain struct {
SoftwareRepository SoftwareRepository
ReleaseRepository ReleaseRepository
DownloadService DownloadService
SoftwareUpdaterService SoftwareUpdaterService
SoftwareService SoftwareService
}
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,
}
softwareUpdaterService := &softwareUpdaterService{tic80Updater: tic80Updater}
downloadServiceInstance := &downloadService{
softwareRepository: softwareRepository,
releaseRepository: releaseRepository,
}
return Domain{
SoftwareRepository: softwareRepository,
ReleaseRepository: releaseRepository,
DownloadService: downloadServiceInstance,
SoftwareUpdaterService: softwareUpdaterService,
SoftwareService: softwareService,
}
}
+12
View File
@@ -0,0 +1,12 @@
package domain
import (
"gorm.io/gorm"
)
func MigrateGoDatabase(db *gorm.DB) {
db.AutoMigrate(
Software{},
Release{},
)
}
+12
View File
@@ -0,0 +1,12 @@
package domain
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:"-"`
SourcePath string `gorm:"size:255" json:"-"`
}
+16
View File
@@ -0,0 +1,16 @@
package domain
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"`
Desc string `gorm:"type:text" json:"desc"`
Site string `gorm:"size:255" json:"site"`
License string `gorm:"size:128" json:"license"`
Platform string `gorm:"size:128" json:"license"`
Releases []Release `json:"releases"`
}
+34
View File
@@ -0,0 +1,34 @@
package domain
import "gorm.io/gorm"
type ReleaseRepository interface {
Create(release *Release) error
FindLatestBySoftwareID(softwareID uint) (*Release, error)
FindBySoftwareIDAndVersion(softwareID uint, version string) (*Release, error)
}
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 {
return r.db.Create(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) {
var release Release
err := r.db.Where("software_id = ? AND version = ?", softwareID, version).First(&release).Error
return &release, err
}
+39
View File
@@ -0,0 +1,39 @@
package domain
import "gorm.io/gorm"
type SoftwareRepository interface {
List() ([]Software, error)
GetByName(name string) (*Software, error)
UpdateOrCreate(software *Software) error
}
type softwareRepository struct {
db *gorm.DB
}
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
}
return &software, nil
}
func (r *softwareRepository) List() ([]Software, error) {
var softwares []Software
if err := r.db.Preload("Releases").Find(&softwares).Error; err != nil {
return nil, err
}
return softwares, nil
}
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
return r.db.Model(&existing).Updates(software).Error
} else {
return r.db.Create(software).Error
}
}
+49
View File
@@ -0,0 +1,49 @@
package domain
import (
"errors"
)
var (
ErrSoftwareNotFound = errors.New("software not found")
ErrNoReleasesFound = errors.New("no releases found for software")
ErrReleaseNotFound = errors.New("release not found for software and version")
)
type DownloadService interface {
GetLatestRelease(softwareName string) (*Release, error)
GetSpecificRelease(softwareName string, version string) (*Release, error)
}
type downloadService struct {
softwareRepository SoftwareRepository
releaseRepository ReleaseRepository
}
func (s *downloadService) GetLatestRelease(softwareName string) (*Release, error) {
software, err := s.softwareRepository.GetByName(softwareName)
if err != nil {
return nil, ErrSoftwareNotFound
}
release, err := s.releaseRepository.FindLatestBySoftwareID(software.ID)
if err != nil {
return nil, ErrNoReleasesFound
}
return release, nil
}
func (s *downloadService) GetSpecificRelease(softwareName string, version string) (*Release, error) {
software, err := s.softwareRepository.GetByName(softwareName)
if err != nil {
return nil, ErrSoftwareNotFound
}
release, err := s.releaseRepository.FindBySoftwareIDAndVersion(software.ID, version)
if err != nil {
return nil, ErrReleaseNotFound
}
return release, nil
}
+43
View File
@@ -0,0 +1,43 @@
package domain
import "sort"
type SoftwareListDTO struct {
Software
LatestRelease *Release
}
type SoftwareService interface {
List() ([]SoftwareListDTO, error)
GetByNameWithReleases(name string) (*Software, error)
}
type softwareService struct {
softwareRepository SoftwareRepository
}
func (s *softwareService) List() ([]SoftwareListDTO, error) {
softwares, err := s.softwareRepository.List()
if err != nil {
return nil, err
}
dtos := make([]SoftwareListDTO, 0, len(softwares))
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)
})
dto.LatestRelease = &sw.Releases[0]
}
dtos = append(dtos, dto)
}
return dtos, nil
}
func (s *softwareService) GetByNameWithReleases(name string) (*Software, error) {
return s.softwareRepository.GetByName(name)
}
+22
View File
@@ -0,0 +1,22 @@
package domain
import (
"fmt"
)
// Keep os for getting GAME_PATH in TIC80 service if needed
type SoftwareUpdaterService interface {
UpdateSoftware(platform, name string) error
}
type softwareUpdaterService struct {
tic80Updater SoftwareUpdaterTIC80Service
}
func (s *softwareUpdaterService) UpdateSoftware(platform, name string) error {
if platform == "tic80" {
return s.tic80Updater.UpdateTIC80Software(name)
}
return fmt.Errorf("unsupported platform: %s", platform)
}
+193
View File
@@ -0,0 +1,193 @@
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 softwareUpdaterTIC80Service struct {
softwareRepository SoftwareRepository
releaseRepository ReleaseRepository
}
func (s *softwareUpdaterTIC80Service) UpdateTIC80Software(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"
// 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 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 {
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
}
}
return nil
}
func (s *softwareUpdaterTIC80Service) parseMeta(path string, name string) (Software, string) {
file, _ := os.Open(path)
defer file.Close()
var g 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
}
}
return g, version
}