upload sources

This commit is contained in:
Sergey Zhemoytel 2024-11-25 23:25:55 +03:00
parent fc83c21c2e
commit 2c002e77ae
8 changed files with 1036 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.*~
*~
.env
cache.json
bin/*

16
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go"
}
]
}

16
Dockerfile Normal file
View file

@ -0,0 +1,16 @@
FROM docker.io/golang:1.23-alpine AS builder
ENV GOEXPORT=https://proxy.golang.org
COPY . /build
RUN cd /build && \
go get ./... && \
go build -ldflags '-s -w' -o abfmigrator && \
go test ./...
FROM scratch
COPY --from=builder /build/abfmigrator .
ENTRYPOINT ["./abfmigrator"]

View file

@ -3,3 +3,57 @@
Утилита миграции репозиториев, которая создает зеркала на в основанных на gitea репозиториях расположенных в abf
(abf.io, abf.openmandriva.org)
## Сборка
```bash
export GOPROXY=direct
go get ./...
go build -o abfmigrator .
```
## Тесты
```bash
go test ./...
```
## Использование
Создать в папке с программой файл `.env`
```
ABF_API_URL=https://abf.miproject.org
ABF_FILE_STORE_URL=https://abf.miproject.org
ABF_LOGIN=user
ABF_PASSWORD=password
GITEA_API_URL=https://mygitea.org/api/v1
GITEA_TOKEN=token_user_gitea
GITEA_OWNER=owner_gitea
PLATFORM_ID=abf_id_platform
REPO_ID=abf_id_repo
OWNER_NAME=abf_owner
```
где
1. `ABF_API_URL` - адрес ABF
2. `ABF_FILE_STORE_URL` - адрес файлового хранилища (не используется)
3. `ABF_LOGIN` - имя пользователя для входа в ABF (login)
4. `ABF_PASSWORD` - пароль пользователя ABF
5. `GITEA_API_URL` - адрес gitea
6. `GITEA_TOKEN` - токен пользователя gitea для доступа к API
7. `GITEA_OWNER` - владелец репозитория, часто пользователь или организация
8. `PLATFORM_ID` - ID платформы в ABF
9. `REPO_ID` - ID репозитория в ABF
10. `OWNER_NAME` - владелец платформы и репозитрия в ABF
## В docker
1. собрать контейнер
```
docker build --network=host -t abfmigrator .
```
2. запустить контейнер
```
docker run -v $PWD/.env:/.env abfmigrator
```

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module abfmigration
go 1.23.3
require github.com/joho/godotenv v1.5.1

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=

798
main.go Normal file
View file

@ -0,0 +1,798 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
)
// AbfJson структура для взаимодействия с API ABF.io
type AbfJson struct {
AbfURL string
FileStoreURL string
Login string
Password string
Base64AuthString string
Log *log.Logger
CacheData map[string][]byte
CacheEtags map[string]string
}
// Ошибки API ABF.io
var errorsMap = map[string]error{
"Invalid email or password.": fmt.Errorf("Authentication error"),
"403 Forbidden | Rate Limit Exceeded": fmt.Errorf("Rate limit exceeded"),
"Page not found": fmt.Errorf("Page not found"),
"Project has not been forked. Name has already been taken": fmt.Errorf("Name already taken"),
"Error 404. Resource not found!": fmt.Errorf("Resource not found"),
"Something went wrong. We've been notified about this issue and we'll take a look at it shortly.": fmt.Errorf("Internal server error"),
"We update the site, it will take some time. We are really trying to do it fast. We apologize for any inconvenience..": fmt.Errorf("Server under maintenance"),
"Requires authentication": fmt.Errorf("Authentication required"),
"Forbidden. Sorry, you don't have enough rights for this action!": fmt.Errorf("Forbidden"),
"Access violation to this page!": fmt.Errorf("Access violation"),
"Bad Request": fmt.Errorf("Bad request"),
}
// GoodMessages сообщения, которые не считаются ошибками
var goodMessages = []string{
"Errors during build publishing!",
"Build is queued for publishing",
}
// FatalErrors фатальные ошибки, которые приводят к выходу из программы
var fatalErrors = []error{
errorsMap["Invalid email or password."],
errorsMap["403 Forbidden | Rate Limit Exceeded"],
errorsMap["Something went wrong. We've been notified about this issue and we'll take a look at it shortly."],
}
// Project структура для хранения информации о проекте
type Project struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
// ProjectDetails структура для хранения деталей проекта
type ProjectDetails struct {
ID int `json:"id"`
Name string `json:"name"`
URL string `json:"git_url"`
WebURL string `json:"web_url"`
SSHURLToRepo string `json:"ssh_url_to_repo"`
HTTPURLToRepo string `json:"http_url_to_repo"`
}
// CacheEntry структура для хранения данных кеша
type CacheEntry struct {
Data map[string][]byte `json:"data"`
CacheEtags map[string]string `json:"cache_etags"`
Timestamp time.Time `json:"timestamp"`
}
// NewAbfJson создает новый экземпляр AbfJson
func NewAbfJson(abfURL, fileStoreURL, login, password string, log *log.Logger) (*AbfJson, error) {
if !strings.HasPrefix(fileStoreURL, "http://") && !strings.HasPrefix(fileStoreURL, "https://") {
log.Fatalf("File-store URL has to start with 'http(s)://'")
}
lpw := fmt.Sprintf("%s:%s", login, password)
encodedLpw := base64.StdEncoding.EncodeToString([]byte(lpw))
return &AbfJson{
AbfURL: strings.TrimSuffix(abfURL, "/"),
FileStoreURL: strings.TrimSuffix(fileStoreURL, "/"),
Login: login,
Password: password,
Base64AuthString: encodedLpw,
Log: log,
CacheData: make(map[string][]byte),
CacheEtags: make(map[string]string),
}, nil
}
// LoadCache загружает кеш из файла
func (a *AbfJson) LoadCache(cacheFilePath string) error {
data, err := ioutil.ReadFile(cacheFilePath)
if err != nil {
if os.IsNotExist(err) {
a.Log.Println("Cache file does not exist. Proceeding without cache.")
return nil
}
return fmt.Errorf("failed to read cache file: %v", err)
}
var cacheEntry CacheEntry
if err := json.Unmarshal(data, &cacheEntry); err != nil {
return fmt.Errorf("failed to unmarshal cache data: %v", err)
}
a.CacheData = cacheEntry.Data
a.CacheEtags = cacheEntry.CacheEtags
a.Log.Println("Cache loaded successfully.")
return nil
}
// SaveCache сохраняет кеш в файл
func (a *AbfJson) SaveCache(cacheFilePath string) error {
cacheEntry := CacheEntry{
Data: a.CacheData,
CacheEtags: a.CacheEtags,
Timestamp: time.Now(),
}
data, err := json.Marshal(cacheEntry)
if err != nil {
return fmt.Errorf("failed to marshal cache data: %v", err)
}
if err := ioutil.WriteFile(cacheFilePath, data, 0644); err != nil {
return fmt.Errorf("failed to write cache file: %v", err)
}
a.Log.Println("Cache saved successfully.")
return nil
}
// IsCacheValid проверяет, действителен ли кеш
func (a *AbfJson) IsCacheValid(cacheFilePath string, ttl time.Duration) (bool, error) {
data, err := ioutil.ReadFile(cacheFilePath)
if err != nil {
if os.IsNotExist(err) {
a.Log.Println("Cache file does not exist. Cache is invalid.")
return false, nil
}
return false, fmt.Errorf("failed to read cache file: %v", err)
}
var cacheEntry CacheEntry
if err := json.Unmarshal(data, &cacheEntry); err != nil {
return false, fmt.Errorf("failed to unmarshal cache data: %v", err)
}
if time.Since(cacheEntry.Timestamp) > ttl {
a.Log.Println("Cache has expired.")
return false, nil
}
a.Log.Println("Cache is valid.")
return true, nil
}
// ProcessResponse обрабатывает ответ API ABF.io
func (a *AbfJson) ProcessResponse(responseString []byte) (map[string]interface{}, error) {
var res map[string]interface{}
if err := json.Unmarshal(responseString, &res); err != nil {
a.Log.Fatalf("Internal server error: it has returned non-json data.")
}
message, ok := res["message"].(string)
if ok && !contains(goodMessages, message) {
return nil, errorsMap[message]
}
repository, repoOk := res["repository"].(map[string]interface{})
project, projOk := res["project"].(map[string]interface{})
if repoOk {
repoMessage, ok := repository["message"].(string)
if ok && (strings.Contains(repoMessage, "error") || strings.Contains(repoMessage, "has not been")) {
return nil, errorsMap[repoMessage]
}
}
if projOk {
projMessage, ok := project["message"].(string)
if ok && (strings.Contains(projMessage, "error") || strings.Contains(projMessage, "has not been")) {
return nil, errorsMap[projMessage]
}
}
if errStr, ok := res["error"].(string); ok && !contains(goodMessages, errStr) {
return nil, errorsMap[errStr]
}
return res, nil
}
// GetURLContents выполняет HTTP-запрос к API ABF.io
func (a *AbfJson) GetURLContents(path string, params map[string]string, method string, body interface{}) (interface{}, error) {
url := a.AbfURL + path
if method == "file_store" {
url = a.FileStoreURL + path
}
if len(params) > 0 {
query := ""
for key, value := range params {
if query == "" {
query += "?"
} else {
query += "&"
}
query += fmt.Sprintf("%s=%s", key, value)
}
url += query
}
a.Log.Printf("Fetching URL: %s", url)
var req *http.Request
var err error
var requestBody []byte
if body != nil {
requestBody, err = json.Marshal(body)
if err != nil {
a.Log.Fatalf("Failed to marshal request body: %v", err)
}
var prettyJSON bytes.Buffer
json.Indent(&prettyJSON, requestBody, "", " ")
a.Log.Printf("Request Body: %s", prettyJSON.String()) // Логирование отправляемого JSON
}
switch method {
case "POST":
req, err = http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
case "PUT":
req, err = http.NewRequest("PUT", url, bytes.NewBuffer(requestBody))
case "DELETE":
req, err = http.NewRequest("DELETE", url, bytes.NewBuffer(requestBody))
default:
req, err = http.NewRequest("GET", url, nil)
}
if err != nil {
a.Log.Fatalf("Failed to create request: %v", err)
}
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", a.Base64AuthString))
req.Header.Add("Content-Type", "application/json")
etag, etagFound := a.CacheEtags[url]
if etagFound {
req.Header.Add("If-None-Match", etag)
}
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
a.Log.Fatalf("HTTP request failed: %v", err)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
a.Log.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode == 304 && etagFound {
a.Log.Println("Getting cached result (cache was validated)")
return a.ProcessResponse(a.CacheData[etag])
}
if resp.StatusCode != 200 && resp.StatusCode != 201 {
a.Log.Printf("Return code: %d", resp.StatusCode)
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(responseBody))
}
etagNew := resp.Header.Get("ETag")
if etagNew != "" {
a.Log.Printf("Caching the new value for %s. ETag is %s", url, etagNew)
a.CacheEtags[url] = etagNew
a.CacheData[etagNew] = responseBody
}
var res interface{}
if err := json.Unmarshal(responseBody, &res); err != nil {
a.Log.Fatalf("Failed to unmarshal response: %v", err)
}
return res, nil
}
// GetProjectsList получает список проектов с обходом всех страниц
func (a *AbfJson) GetProjectsList(platformID, repoID int, ownerName string) ([]Project, error) {
var allProjects []Project
page := 1
perPage := 100
for {
params := map[string]string{
"added": "true",
"format": "json",
"owner_name": ownerName,
"page": fmt.Sprintf("%d", page),
"per_page": fmt.Sprintf("%d", perPage),
"project_name": "",
}
path := fmt.Sprintf("/platforms/%d/repositories/%d/projects_list", platformID, repoID)
res, err := a.GetURLContents(path, params, "GET", nil)
if err != nil {
return nil, fmt.Errorf("Failed to get projects list: %v", err)
}
// Преобразуем res["projects"] в []byte
projectsArray, ok := res.(map[string]interface{})["projects"]
if !ok {
a.Log.Fatalf("Failed to find 'projects' in response")
}
projectsBytes, err := json.Marshal(projectsArray)
if err != nil {
a.Log.Fatalf("Failed to marshal projects list: %v", err)
}
var projects []Project
if err := json.Unmarshal(projectsBytes, &projects); err != nil {
a.Log.Fatalf("Failed to unmarshal projects list: %v", err)
}
allProjects = append(allProjects, projects...)
if len(projects) < perPage {
break
}
page++
}
return allProjects, nil
}
// GetProjectDetails получает детали проекта по ID
func (a *AbfJson) GetProjectDetails(projectID int) (*ProjectDetails, error) {
path := fmt.Sprintf("/api/v1/projects/%d.json", projectID)
res, err := a.GetURLContents(path, nil, "GET", nil)
if err != nil {
a.Log.Printf("Failed to get project details for project ID %d: %v", projectID, err)
return nil, err
}
projectData, ok := res.(map[string]interface{})["project"].(map[string]interface{})
if !ok {
a.Log.Printf("Missing 'project' field in response for project ID %d", projectID)
return nil, fmt.Errorf("Missing 'project' field in response")
}
id, idOk := projectData["id"]
if !idOk {
a.Log.Printf("Missing 'id' field in project details for project ID %d", projectID)
return nil, fmt.Errorf("Missing 'id' field in project details")
}
idFloat, idOk := id.(float64)
if !idOk {
a.Log.Printf("Invalid 'id' type in project details for project ID %d: expected float64, got %T", projectID, id)
return nil, fmt.Errorf("Invalid 'id' type in project details")
}
name, nameOk := projectData["name"].(string)
if !nameOk {
a.Log.Printf("Missing 'name' field in project details for project ID %d", projectID)
return nil, fmt.Errorf("Missing 'name' field in project details")
}
gitUrl, gitUrlOk := projectData["git_url"].(string)
webUrl, webUrlOk := projectData["web_url"].(string)
sshUrlToRepo, sshUrlToRepoOk := projectData["ssh_url_to_repo"].(string)
httpUrlToRepo, httpUrlToRepoOk := projectData["http_url_to_repo"].(string)
var url string
if gitUrlOk {
url = gitUrl
} else if httpUrlToRepoOk {
url = httpUrlToRepo
} else if webUrlOk {
url = webUrl
} else if sshUrlToRepoOk {
url = sshUrlToRepo
} else {
a.Log.Printf("Missing URL fields in project details for project ID %d", projectID)
return nil, fmt.Errorf("Missing URL fields in project details")
}
projectDetails := &ProjectDetails{
ID: int(idFloat),
Name: name,
URL: url,
WebURL: webUrl,
SSHURLToRepo: sshUrlToRepo,
HTTPURLToRepo: httpUrlToRepo,
}
return projectDetails, nil
}
// contains проверяет, содержит ли слайс строк строку
func contains(slice []string, str string) bool {
for _, item := range slice {
if item == str {
return true
}
}
return false
}
// GiteaClient структура для взаимодействия с API Gitea
type GiteaClient struct {
APIURL string
Token string
Owner string
Log *log.Logger
}
// Repository структура для хранения информации о репозитории в Gitea
type Repository struct {
ID int `json:"id"`
Name string `json:"name"`
}
// GetRepositories получает список репозиториев из Gitea
func (g *GiteaClient) GetRepositories() ([]Repository, error) {
path := "/user/repos"
res, err := g.GetURLContents(path, nil, "GET", nil)
if err != nil {
return nil, fmt.Errorf("Failed to get repositories: %v", err)
}
// Проверяем, что res является массивом
reposArray, ok := res.([]interface{})
if !ok {
g.Log.Fatalf("Expected array in response, got %T", res)
}
var repos []Repository
for _, repoInterface := range reposArray {
repoMap, ok := repoInterface.(map[string]interface{})
if !ok {
g.Log.Fatalf("Expected map in array element, got %T", repoInterface)
}
id, idOk := repoMap["id"].(float64)
if !idOk {
g.Log.Fatalf("Invalid 'id' type in repository: expected float64, got %T", repoMap["id"])
}
name, nameOk := repoMap["name"].(string)
if !nameOk {
g.Log.Fatalf("Missing 'name' field in repository")
}
repos = append(repos, Repository{
ID: int(id),
Name: name,
})
}
return repos, nil
}
// MirrorRepositoryPayload структура для запроса зеркалирования репозитория в Gitea
type MirrorRepositoryPayload struct {
AuthPassword string `json:"auth_password,omitempty"`
AuthToken string `json:"auth_token,omitempty"`
AuthUsername string `json:"auth_username,omitempty"`
CloneAddr string `json:"clone_addr"`
Description string `json:"description,omitempty"`
Issues bool `json:"issues"`
Labels bool `json:"labels"`
LFS bool `json:"lfs"`
LFSEndpoint string `json:"lfs_endpoint,omitempty"`
Milestones bool `json:"milestones"`
Mirror bool `json:"mirror"`
MirrorInterval string `json:"mirror_interval,omitempty"`
Private bool `json:"private"`
PullRequests bool `json:"pull_requests"`
Releases bool `json:"releases"`
RepoName string `json:"repo_name"`
RepoOwner string `json:"repo_owner"`
Service string `json:"service"`
UID int `json:"uid,omitempty"`
Wiki bool `json:"wiki"`
}
// MirrorRepository создает зеркало репозитория в Gitea
func (g *GiteaClient) MirrorRepository(repoName, cloneURL string) error {
payload := MirrorRepositoryPayload{
CloneAddr: cloneURL,
Description: "",
Issues: true,
Labels: true,
LFS: false,
Milestones: true,
Mirror: true,
MirrorInterval: "8h0m0s", // Интервал зеркалирования
Private: false,
PullRequests: true,
Releases: true,
RepoName: repoName,
RepoOwner: g.Owner,
Service: "git",
Wiki: true,
}
path := "/repos/migrate"
res, err := g.GetURLContents(path, nil, "POST", payload)
if err != nil {
return fmt.Errorf("Failed to mirror repository: %v", err)
}
if res == nil {
return fmt.Errorf("Empty response from Gitea")
}
g.Log.Printf("Response from Gitea: %+v", res)
return nil
}
// GetURLContents выполняет HTTP-запрос к API Gitea
func (g *GiteaClient) GetURLContents(path string, params map[string]string, method string, body interface{}) (interface{}, error) {
url := g.APIURL + path
if len(params) > 0 {
query := ""
for key, value := range params {
if query == "" {
query += "?"
} else {
query += "&"
}
query += fmt.Sprintf("%s=%s", key, value)
}
url += query
}
g.Log.Printf("Fetching URL: %s", url)
var req *http.Request
var err error
var requestBody []byte
if body != nil {
requestBody, err = json.Marshal(body)
if err != nil {
g.Log.Fatalf("Failed to marshal request body: %v", err)
}
var prettyJSON bytes.Buffer
json.Indent(&prettyJSON, requestBody, "", " ")
g.Log.Printf("Request Body: %s", prettyJSON.String()) // Логирование отправляемого JSON
}
switch method {
case "POST":
req, err = http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
case "PUT":
req, err = http.NewRequest("PUT", url, bytes.NewBuffer(requestBody))
case "DELETE":
req, err = http.NewRequest("DELETE", url, bytes.NewBuffer(requestBody))
default:
req, err = http.NewRequest("GET", url, nil)
}
if err != nil {
g.Log.Fatalf("Failed to create request: %v", err)
}
req.Header.Add("Authorization", fmt.Sprintf("token %s", g.Token))
req.Header.Add("Content-Type", "application/json")
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
g.Log.Fatalf("HTTP request failed: %v", err)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
g.Log.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != 200 && resp.StatusCode != 201 {
g.Log.Printf("Return code: %d", resp.StatusCode)
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(responseBody))
}
var res interface{}
if err := json.Unmarshal(responseBody, &res); err != nil {
g.Log.Fatalf("Failed to unmarshal response: %v", err)
}
return res, nil
}
func main() {
// Флаги для параметров проектов
platformIDFlag := flag.Int("platformid", 0, "Platform ID")
repoIDFlag := flag.Int("repoid", 0, "Repository ID")
ownerFlag := flag.String("owner", "", "Owner name")
cacheTTLFlag := flag.Duration("cachettl", 12*time.Hour, "Cache TTL duration")
forceFlag := flag.Bool("force", false, "Force refresh cache")
cacheFilePathFlag := flag.String("cachefile", "cache.json", "Cache file path")
flag.Parse()
// Загрузка переменных окружения из файла .env
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
abfURL := os.Getenv("ABF_API_URL")
fileStoreURL := os.Getenv("ABF_FILE_STORE_URL")
login := os.Getenv("ABF_LOGIN")
password := os.Getenv("ABF_PASSWORD")
giteaAPIURL := os.Getenv("GITEA_API_URL")
giteaToken := os.Getenv("GITEA_TOKEN")
giteaOwner := os.Getenv("GITEA_OWNER")
cacheFilePath := *cacheFilePathFlag
// Приоритет флагов над переменными окружения
platformID := *platformIDFlag
if platformID == 0 {
platformIDStr := os.Getenv("PLATFORM_ID")
if platformIDStr == "" {
log.Fatalf("Platform ID is not set either via --platformid flag or PLATFORM_ID environment variable")
}
platformID, err = strconv.Atoi(platformIDStr)
if err != nil {
log.Fatalf("Invalid PLATFORM_ID value: %v", err)
}
}
repoID := *repoIDFlag
if repoID == 0 {
repoIDStr := os.Getenv("REPO_ID")
if repoIDStr == "" {
log.Fatalf("Repository ID is not set either via --repoid flag or REPO_ID environment variable")
}
repoID, err = strconv.Atoi(repoIDStr)
if err != nil {
log.Fatalf("Invalid REPO_ID value: %v", err)
}
}
ownerName := *ownerFlag
if ownerName == "" {
ownerName = os.Getenv("OWNER_NAME")
if ownerName == "" {
log.Fatalf("Owner name is not set either via --owner flag or OWNER_NAME environment variable")
}
}
logger := log.New(os.Stdout, "", log.LstdFlags)
abfClient, err := NewAbfJson(abfURL, fileStoreURL, login, password, logger)
if err != nil {
logger.Fatalf("Failed to create ABF client: %v", err)
}
// Проверка кеша
cacheValid, err := abfClient.IsCacheValid(cacheFilePath, *cacheTTLFlag)
if err != nil {
logger.Fatalf("Failed to check cache validity: %v", err)
}
if cacheValid && !*forceFlag {
logger.Println("Using cached data.")
} else {
logger.Println("Refreshing cache.")
// Получение списка проектов
projects, err := abfClient.GetProjectsList(platformID, repoID, ownerName)
if err != nil {
logger.Fatalf("Failed to get projects: %v", err)
}
logger.Printf("Total projects found: %d", len(projects))
// Сбор ID и имени проектов
projectIDs := make(map[int]string)
for _, project := range projects {
projectIDs[project.ID] = project.Name
}
// Получение деталей проектов и их URL
var projectDetailsList []*ProjectDetails
for projectID, projectName := range projectIDs {
details, err := abfClient.GetProjectDetails(projectID)
if err != nil {
logger.Printf("Failed to get details for project %d (%s): %v", projectID, projectName, err)
continue
}
projectDetailsList = append(projectDetailsList, details)
}
// Сохранение кеша
abfClient.CacheData = make(map[string][]byte)
for _, details := range projectDetailsList {
detailsBytes, err := json.Marshal(details)
if err != nil {
logger.Fatalf("Failed to marshal project details: %v", err)
}
abfClient.CacheData[strconv.Itoa(details.ID)] = detailsBytes
}
if err := abfClient.SaveCache(cacheFilePath); err != nil {
logger.Fatalf("Failed to save cache: %v", err)
}
// Вывод списка проектов с URL
for _, details := range projectDetailsList {
logger.Printf("Project ID: %d, Name: %s, URL: %s", details.ID, details.Name, details.URL)
}
}
// Загрузка кеша для дальнейшей работы
if err := abfClient.LoadCache(cacheFilePath); err != nil {
logger.Fatalf("Failed to load cache: %v", err)
}
// Получение списка репозиториев из Gitea
giteaClient := &GiteaClient{
APIURL: giteaAPIURL,
Token: giteaToken,
Owner: giteaOwner,
Log: logger,
}
giteaRepos, err := giteaClient.GetRepositories()
if err != nil {
logger.Fatalf("Failed to get repositories from Gitea: %v", err)
}
giteaRepoNames := make(map[string]struct{})
for _, repo := range giteaRepos {
giteaRepoNames[repo.Name] = struct{}{}
}
// Миграция репозиториев в Gitea
for projectIDStr, detailsBytes := range abfClient.CacheData {
var projectDetails ProjectDetails
if err := json.Unmarshal(detailsBytes, &projectDetails); err != nil {
logger.Printf("Failed to unmarshal project details: %v", err)
continue
}
projectID, err := strconv.Atoi(projectIDStr)
if err != nil {
logger.Printf("Недопустимый идентификатор проекта %s: %v", projectIDStr, err)
continue
}
// Используйте переменную projectID здесь
logger.Printf("Идентификатор проекта: %d", projectID)
repoName := projectDetails.Name
cloneURL := projectDetails.URL
if _, exists := giteaRepoNames[repoName]; exists {
logger.Printf("Repository %s already exists in Gitea. Skipping...", repoName)
continue
}
err = giteaClient.MirrorRepository(repoName, cloneURL)
if err != nil {
logger.Printf("Failed to mirror repository %s: %v", repoName, err)
} else {
logger.Printf("Repository %s mirrored to Gitea.", repoName)
}
}
}

140
main_test.go Normal file
View file

@ -0,0 +1,140 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"log"
)
// Тестирование метода MirrorRepository с неверным телом запроса
func TestMirrorRepositoryInvalidPayload(t *testing.T) {
// Создаем мок сервера
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
rw.WriteHeader(http.StatusMethodNotAllowed)
return
}
contentType := req.Header.Get("Content-Type")
if contentType != "application/json" {
rw.WriteHeader(http.StatusBadRequest)
return
}
auth := req.Header.Get("Authorization")
if auth != "token test-token" {
rw.WriteHeader(http.StatusUnauthorized)
return
}
var payload MirrorRepositoryPayload
err := json.NewDecoder(req.Body).Decode(&payload)
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
// Проверяем корректность полей payload
if payload.CloneAddr != "https://djam@abf.io/djam/btunlock.git" ||
payload.RepoName != "btunlock" ||
payload.RepoOwner != "djam" ||
!payload.Issues ||
!payload.Labels ||
payload.LFS ||
!payload.Milestones ||
!payload.Mirror ||
payload.MirrorInterval != "8h0m0s" ||
payload.Private ||
!payload.PullRequests ||
!payload.Releases ||
payload.Service != "git" ||
!payload.Wiki {
rw.WriteHeader(http.StatusBadRequest)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusCreated)
rw.Write([]byte(`{"id": 123, "name": "btunlock", "full_name": "djam/btunlock"}`))
}))
defer server.Close()
// Создаем клиент для тестирования
giteaClient := &GiteaClient{
APIURL: server.URL,
Token: "test-token",
Owner: "djam",
Log: log.New(os.Stdout, "", log.LstdFlags),
}
// Вызываем метод MirrorRepository с неверным телом запроса
err := giteaClient.MirrorRepository("btunlock", "")
if err == nil {
t.Fatalf("Expected error, got nil")
}
// Проверяем, что ошибка соответствует ожидаемому сообщению
expectedErrorMessage := "Failed to mirror repository: HTTP error 400: "
if err.Error() != expectedErrorMessage {
t.Errorf("Expected error message %q, got %q", expectedErrorMessage, err.Error())
}
}
// Тестирование метода MirrorRepository с ошибкой 422
func TestMirrorRepositoryUnprocessableEntity(t *testing.T) {
// Создаем мок сервера
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
rw.WriteHeader(http.StatusMethodNotAllowed)
return
}
contentType := req.Header.Get("Content-Type")
if contentType != "application/json" {
rw.WriteHeader(http.StatusBadRequest)
return
}
auth := req.Header.Get("Authorization")
if auth != "token test-token" {
rw.WriteHeader(http.StatusUnauthorized)
return
}
var payload MirrorRepositoryPayload
err := json.NewDecoder(req.Body).Decode(&payload)
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
rw.WriteHeader(http.StatusUnprocessableEntity)
}))
defer server.Close()
// Создаем клиент для тестирования
giteaClient := &GiteaClient{
APIURL: server.URL,
Token: "test-token",
Owner: "djam",
Log: log.New(os.Stdout, "", log.LstdFlags),
}
// Вызываем метод MirrorRepository с ошибкой 422
err := giteaClient.MirrorRepository("btunlock", "")
if err == nil {
t.Fatalf("Expected error, got nil")
}
// Проверяем, что ошибка соответствует ожидаемому сообщению
expectedErrorMessage := "Failed to mirror repository: HTTP error 422: "
if err.Error() != expectedErrorMessage {
t.Errorf("Expected error message %q, got %q", expectedErrorMessage, err.Error())
}
}