initial commit
This commit is contained in:
Executable
+41
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MigrateGoDatabase(db *gorm.DB) {
|
||||
db.AutoMigrate(
|
||||
Software{},
|
||||
Release{},
|
||||
)
|
||||
}
|
||||
@@ -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:"-"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user