Now repository can have apps and allowed to install them
This commit is contained in:
parent
d004e69e6b
commit
71c1312df9
4 changed files with 512 additions and 7 deletions
28
apktest.go
Normal file
28
apktest.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/avast/apkparser"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
enc := xml.NewEncoder(os.Stdout)
|
||||
//enc.Indent("", "\t")
|
||||
zipErr, resErr, manErr := apkparser.ParseApk(os.Args[1], enc)
|
||||
if zipErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open the APK: %s", zipErr.Error())
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if resErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse resources: %s", resErr.Error())
|
||||
}
|
||||
if manErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse AndroidManifest.xml: %s", manErr.Error())
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
5
go.mod
5
go.mod
|
@ -3,3 +3,8 @@ module fdroidgo
|
|||
go 1.24.2
|
||||
|
||||
require github.com/BurntSushi/toml v1.5.0
|
||||
|
||||
require (
|
||||
github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
)
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1,2 +1,6 @@
|
|||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56 h1:gFlKwiEFHiSUH79HIhSa2jJmM6gZg4xQaZIkMjUS73c=
|
||||
github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56/go.mod h1:3F9A8btIerUcuy7Fmno+g/nIk4ELKJ6NCs2/KK1bvLs=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
|
|
482
main.go
482
main.go
|
@ -8,9 +8,20 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"github.com/BurntSushi/toml"
|
||||
"time"
|
||||
"log"
|
||||
"github.com/BurntSushi/toml"
|
||||
"time"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"github.com/avast/apkparser"
|
||||
"strconv"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"path/filepath"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
|
@ -55,6 +66,7 @@ type Index struct {
|
|||
Repo Repo `json:"repo"`
|
||||
Requests Requests `json:"requests"`
|
||||
Apps []App `json:"apps"`
|
||||
Packages map[string][]Package `json:"packages"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
@ -62,6 +74,389 @@ type Config struct {
|
|||
Icon string `toml:"icon"`
|
||||
Address string `toml:"address"`
|
||||
Description string `toml:"description"`
|
||||
KeyStorePath string `toml:"keystore_path"`
|
||||
KeyStorePassword string `toml:"keystore_password"`
|
||||
KeyStoreAlias string `toml:"keystore_alias"`
|
||||
BinarySources []string `toml:"binarySources"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
type ReleaseAsset struct {
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
ID int `json:"id"`
|
||||
Size int `json:"size"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Uploader *User `json:"uploader,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt *time.Time `json:"published_at,omitempty"`
|
||||
Author User `json:"author"`
|
||||
Assets []ReleaseAsset `json:"assets"`
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
License *License `json:"license"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
IssuesUrl string `json:"issues_url"`
|
||||
Homepage string `json:"homepage"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type License struct {
|
||||
Name string `json:"name"`
|
||||
SPDXID string `json:"spdx_id"`
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
Added int64 `json:"added"`
|
||||
ApkName string `json:"apkName"`
|
||||
Hash string `json:"hash"`
|
||||
HashType string `json:"hashType"`
|
||||
MinSdkVersion int `json:"minSdkVersion"`
|
||||
NativeCode []string `json:"nativecode"`
|
||||
PackageName string `json:"packageName"`
|
||||
Sig string `json:"sig"`
|
||||
Signer string `json:"signer"`
|
||||
Size int64 `json:"size"`
|
||||
TargetSdkVersion int `json:"targetSdkVersion"`
|
||||
UsesPermission [][]*string `json:"uses-permission"`
|
||||
UsesPermissionSdk23 [][]string `json:"uses-permission-sdk-23"`
|
||||
VersionCode int64 `json:"versionCode"`
|
||||
VersionName string `json:"versionName"`
|
||||
ApplicationName string `json:"-"`
|
||||
}
|
||||
type Application struct {
|
||||
Label string `xml:"label,attr"`
|
||||
Icon string `xml:"icon,attr"`
|
||||
// Add other application attributes as needed
|
||||
}
|
||||
type Manifest struct {
|
||||
PackageName string `xml:"package,attr"`
|
||||
VersionCode int64 `xml:"versionCode,attr"`
|
||||
VersionName string `xml:"versionName,attr"`
|
||||
UsesSdk UsesSdk `xml:"uses-sdk"`
|
||||
UsesPermission []UsesPermission `xml:"uses-permission"`
|
||||
Application Application `xml:"application"`
|
||||
}
|
||||
|
||||
// Structure for <uses-sdk>
|
||||
|
||||
type UsesSdk struct {
|
||||
MinSdkVersion string `xml:"minSdkVersion,attr"`
|
||||
TargetSdkVersion string `xml:"targetSdkVersion,attr"`
|
||||
}
|
||||
// Structure for <uses-permission>
|
||||
type UsesPermission struct {
|
||||
Name string `xml:"name,attr"`
|
||||
}
|
||||
|
||||
// Structure for <uses-feature>
|
||||
|
||||
|
||||
func apkExists(filePath string) bool {
|
||||
_, err := os.Stat(filePath)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// Function to check if the URL is an APK
|
||||
func isAPK(url string) bool {
|
||||
return strings.HasSuffix(url, ".apk")
|
||||
}
|
||||
|
||||
// Function to download the APK file
|
||||
func downloadAPK(url string, filePath string) error {
|
||||
fmt.Printf("Downloading %s",url)
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to download file: %s", response.Status)
|
||||
}
|
||||
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, response.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func getResourceTypeId(resourceType string) int {
|
||||
switch resourceType {
|
||||
case "string":
|
||||
return 0
|
||||
case "drawable":
|
||||
return 1
|
||||
case "layout":
|
||||
return 2
|
||||
case "mipmap":
|
||||
return 3
|
||||
case "color":
|
||||
return 4
|
||||
case "id":
|
||||
return 5
|
||||
case "style":
|
||||
return 6
|
||||
case "anim":
|
||||
return 7
|
||||
case "menu":
|
||||
return 8
|
||||
case "xml":
|
||||
return 9
|
||||
case "raw":
|
||||
return 10
|
||||
case "font":
|
||||
return 11
|
||||
case "transition":
|
||||
return 12
|
||||
case "drawable-v21":
|
||||
return 13
|
||||
default:
|
||||
return -1 // Invalid resource type
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func saveIconFile(f *apkparser.ZipReaderFile, packageName string) string {
|
||||
// Create the directory if it does not exist
|
||||
err := os.MkdirAll(packageName + "/en-US", os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating directory:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Define the path for the icon file
|
||||
|
||||
iconPath := filepath.Join(packageName, "en-US")
|
||||
iconPath = filepath.Join(iconPath,"icon.png")
|
||||
// Create a new file to save the icon
|
||||
outFile, err := os.Create(iconPath)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating icon file:", err)
|
||||
return ""
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Open the zip file for reading
|
||||
err = f.Open()
|
||||
if err != nil {
|
||||
fmt.Println("Error opening zip file:", err)
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Copy the contents of the zip file to the new file
|
||||
if _, err := io.Copy(outFile, f); err != nil {
|
||||
fmt.Println("Error copying icon file:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return iconPath
|
||||
}
|
||||
|
||||
func processAPK(filePath string) Package {
|
||||
fmt.Printf("Processing APK: %s\n", filePath)
|
||||
var buf bytes.Buffer
|
||||
enc := xml.NewEncoder(&buf)
|
||||
|
||||
zipFile, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening ZIP file:", err)
|
||||
os.Exit(1)
|
||||
return Package{}
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
// Get the file info
|
||||
fileInfo, err := zipFile.Stat()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting file info:", err)
|
||||
os.Exit(1)
|
||||
return Package{}
|
||||
}
|
||||
|
||||
// Create a zip.Reader
|
||||
zipReader, zipErr := apkparser.OpenZipReader(zipFile)
|
||||
p,resErr := apkparser.NewParser(zipReader,enc)
|
||||
|
||||
manErr := p.ParseXml("AndroidManifest.xml")
|
||||
if zipErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open the APK: %s\n", zipErr.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if resErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse resources: %s\n", resErr.Error())
|
||||
}
|
||||
if manErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to parse AndroidManifest.xml: %s\n", manErr.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
manifestXML := buf.String()
|
||||
var manifest Manifest
|
||||
if err := xml.Unmarshal([]byte(manifestXML), &manifest); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to unmarshal manifest XML: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
minsdk, err := strconv.Atoi(manifest.UsesSdk.MinSdkVersion)
|
||||
if err != nil {
|
||||
log.Fatal("Can't parse minsdk: ", manifest.UsesSdk.MinSdkVersion)
|
||||
os.Exit(1)
|
||||
}
|
||||
targetsdk, err := strconv.Atoi(manifest.UsesSdk.TargetSdkVersion)
|
||||
if err != nil {
|
||||
log.Fatal("Can't parse targetsdk: ", manifest.UsesSdk.TargetSdkVersion)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Calculate SHA-256 hash of the APK file
|
||||
hash, err := calculateSHA256(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to calculate SHA-256 hash: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
packageInfo := Package{
|
||||
ApkName: filePath,
|
||||
VersionCode: manifest.VersionCode,
|
||||
VersionName: manifest.VersionName,
|
||||
MinSdkVersion: minsdk,
|
||||
TargetSdkVersion: targetsdk,
|
||||
PackageName: manifest.PackageName,
|
||||
UsesPermission: make([][]*string, len(manifest.UsesPermission)),
|
||||
ApplicationName: manifest.Application.Label,
|
||||
Hash: hash,
|
||||
HashType: "SHA-256",
|
||||
Size: fileInfo.Size(),
|
||||
}
|
||||
|
||||
for i, perm := range manifest.UsesPermission {
|
||||
packageInfo.UsesPermission[i] = append(packageInfo.UsesPermission[i], &perm.Name)
|
||||
packageInfo.UsesPermission[i] = append(packageInfo.UsesPermission[i], nil)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(manifest.Application.Icon, "@") {
|
||||
iconFile := zipReader.File[manifest.Application.Icon]
|
||||
saveIconFile(iconFile,manifest.PackageName)
|
||||
}
|
||||
return packageInfo
|
||||
}
|
||||
|
||||
// calculateSHA256 computes the SHA-256 hash of the given file.
|
||||
func calculateSHA256(filePath string) (string, error) {
|
||||
fmt.Println("Calculating SHA256")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := hasher.Sum(nil)
|
||||
return hex.EncodeToString(hash), nil
|
||||
}
|
||||
|
||||
func getFilenameFromURL(urlStr string) (string, error) {
|
||||
// Parse the URL
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get the path from the URL and extract the filename
|
||||
filename := path.Base(parsedURL.Path)
|
||||
return filename, nil
|
||||
}
|
||||
// Function to handle downloading and processing APKs from a release
|
||||
func handleRelease(release Release) []Package {
|
||||
var packages []Package
|
||||
for _, asset := range release.Assets {
|
||||
if !isAPK(asset.BrowserDownloadURL) {
|
||||
//fmt.Printf("Skipping non-APK file: %s\n", asset.BrowserDownloadURL)
|
||||
continue
|
||||
}
|
||||
|
||||
filePath,err := getFilenameFromURL(asset.BrowserDownloadURL)
|
||||
if apkExists(asset.BrowserDownloadURL) {
|
||||
fmt.Printf("APK already exists: %s\n", asset.BrowserDownloadURL)
|
||||
continue
|
||||
}
|
||||
|
||||
err = downloadAPK(asset.BrowserDownloadURL, filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error downloading APK: %v\n", err)
|
||||
os.Exit(1)
|
||||
continue
|
||||
}
|
||||
|
||||
packageInfo := processAPK(filePath)
|
||||
packages = append(packages,packageInfo)
|
||||
}
|
||||
return packages
|
||||
}
|
||||
func getReleases(repo string) ([]Release, error) {
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get releases: %s", resp.Status)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var releases []Release
|
||||
// Декодируем JSON-ответ в структуру Release
|
||||
// Декодируем JSON-ответ в pструктуру Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
|
||||
func getRepositoryInfo(repo string) (Repository, error) {
|
||||
fmt.Println("Fetching repository info")
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s", repo)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return Repository{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return Repository{}, fmt.Errorf("failed to get repository info: %s", resp.Status)
|
||||
}
|
||||
|
||||
var repository Repository
|
||||
if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil {
|
||||
return Repository{}, err
|
||||
}
|
||||
|
||||
return repository, nil
|
||||
}
|
||||
|
||||
func generateJSON(index Index, filename string) error {
|
||||
|
@ -114,7 +509,48 @@ func signJarWithJarsigner(jarFile string, keystore string, password string, alia
|
|||
return err, string(output) // Возвращаем ошибку и вывод как строку
|
||||
}
|
||||
|
||||
func createAppFromReleases(releases []Release, repo string) (App, []Package,error) {
|
||||
// Получаем информацию о репозитории
|
||||
repository, err := getRepositoryInfo(repo)
|
||||
if err != nil {
|
||||
return App{}, []Package{}, err
|
||||
}
|
||||
|
||||
// Предположим, что мы берем данные из первого релиза
|
||||
if len(releases) == 0 {
|
||||
return App{}, []Package{}, nil
|
||||
}
|
||||
|
||||
release := releases[0]
|
||||
packages := handleRelease(release)
|
||||
|
||||
app := App{
|
||||
AuthorName: release.Author.Login,
|
||||
Categories: []string{"fdroid"},
|
||||
SuggestedVersionCode: release.TagName,
|
||||
IssueTracker: repository.HTMLURL + "/issues",
|
||||
License: repository.License.SPDXID,
|
||||
Name: packages[0].ApplicationName,
|
||||
SourceCode: repository.HTMLURL,
|
||||
Summary: repository.Description,
|
||||
WebSite: repository.Homepage,
|
||||
Added: time.Now().Unix(),
|
||||
PackageName: packages[0].PackageName,
|
||||
LastUpdated: release.CreatedAt.Unix(),
|
||||
Localized: Localized{
|
||||
ENUS: Icon {
|
||||
Icon: "icon.png",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return app, packages, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var apps []App
|
||||
var packages map[string][]Package
|
||||
packages = make(map[string][]Package)
|
||||
var config Config
|
||||
file, err := os.Open("config.toml")
|
||||
if err != nil {
|
||||
|
@ -126,6 +562,37 @@ func main() {
|
|||
if _, err := toml.NewDecoder(file).Decode(&config); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, source := range config.BinarySources {
|
||||
fmt.Printf("Processing %s\n",source)
|
||||
// Извлекаем имя репозитория из ссылки
|
||||
repo := strings.TrimPrefix(source, "github.com/")
|
||||
if repo == source { // Если не удалось извлечь, пропускаем
|
||||
fmt.Printf("Invalid repository format: %s\n", source)
|
||||
continue
|
||||
}
|
||||
|
||||
// Получаем релизы для репозитория
|
||||
fmt.Println("Fetching releases")
|
||||
releases, err := getReleases(repo)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching releases for %s: %v\n", repo, err)
|
||||
os.Exit(1)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
|
||||
var app App
|
||||
fmt.Println("Creating app object from repository info and apk")
|
||||
app,app_packages, err := createAppFromReleases(releases,repo)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create app from releases: ",err)
|
||||
}
|
||||
apps = append(apps,app)
|
||||
packages[app_packages[0].PackageName] = append(packages[app_packages[0].PackageName], app_packages...)
|
||||
}
|
||||
|
||||
index := Index {
|
||||
Repo: Repo{
|
||||
Timestamp: time.Now().Unix(),
|
||||
|
@ -139,7 +606,8 @@ func main() {
|
|||
Install: []string{},
|
||||
Uninstall: []string{},
|
||||
},
|
||||
Apps: []App{},
|
||||
Apps: apps,
|
||||
Packages: packages,
|
||||
}
|
||||
// Генерация JSON файла
|
||||
err = generateJSON(index, "index-v1.json")
|
||||
|
@ -158,9 +626,9 @@ func main() {
|
|||
fmt.Println("JAR file created successfully.")
|
||||
|
||||
// Подпись JAR файла
|
||||
p12File := "../../keystore.p12" // Укажите путь к вашему P12 файлу
|
||||
password := "u86viXB0FRlTY2K1buzGqWHdDu6pIgu8520R3IJgXAE=" // Укажите пароль для P12 файла
|
||||
alias := "curious-invention.aeza.network" // Укажите алиас для ключа
|
||||
p12File := config.KeyStorePath // Укажите путь к вашему P12 файлу
|
||||
password := config.KeyStorePassword // Укажите пароль для P12 файла
|
||||
alias := config.KeyStoreAlias // Укажите алиас для ключа
|
||||
|
||||
err, output := signJarWithJarsigner("index-v1.jar", p12File, password, alias)
|
||||
if err != nil {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue