Skip to content

Commit f90aa81

Browse files
authored
Merge pull request #8 from MichailKon/dev/storage
Dev/storage
2 parents 1f23f2f + 6b84fe5 commit f90aa81

File tree

21 files changed

+761
-52
lines changed

21 files changed

+761
-52
lines changed

common/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Config struct {
1616

1717
Invoker *InvokerConfig `yaml:"Invoker,omitempty"`
1818
Master *MasterConfig `yaml:"Master,omitempty"`
19+
Storage *StorageConfig `yaml:"Storage,omitempty"`
1920
// TODO: Add instances here
2021

2122
DB DBConfig `yaml:"DB"`
@@ -48,5 +49,6 @@ func fillInConfig(config *Config) {
4849

4950
fillInConnections(config)
5051
fillInMasterConfig(config.Master)
52+
fillInStorageConfig(config.Storage)
5153
FillInInvokerConfig(config.Invoker)
5254
}

common/config/storage.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package config
2+
3+
type StorageConfig struct {
4+
StoragePath string `yaml:"StoragePath"`
5+
6+
BlockSize uint `yaml:"BlockSize"`
7+
}
8+
9+
func fillInStorageConfig(config *StorageConfig) {
10+
if len(config.StoragePath) == 0 {
11+
panic("No storage path specified")
12+
}
13+
14+
if config.BlockSize == 0 {
15+
config.BlockSize = 3
16+
}
17+
}
Lines changed: 149 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,168 @@
11
package storageconn
22

33
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"mime"
8+
"net/http"
9+
"os"
10+
"path/filepath"
411
"testing_system/common/config"
12+
"testing_system/common/connectors"
513
)
614

715
type Connector struct {
8-
// TODO: Add storage data loader
16+
connection *connectors.ConnectorBase
917
}
1018

1119
func NewConnector(connection *config.Connection) *Connector {
12-
return nil
20+
if connection == nil {
21+
return nil
22+
}
23+
return &Connector{connectors.NewConnectorBase(connection)}
1324
}
1425

15-
func (s *Connector) Download(request *Request) *ResponseFiles {
16-
return nil
26+
func (s *Connector) Download(request *Request) *FileResponse {
27+
response := NewFileResponse(*request)
28+
29+
if err := os.MkdirAll(request.BaseFolder, 0775); err != nil {
30+
response.Error = fmt.Errorf("failed to create base folder: %v", err)
31+
return response
32+
}
33+
34+
path := "/storage/get"
35+
r := s.connection.R()
36+
37+
requestJSON, err := json.Marshal(request)
38+
if err != nil {
39+
response.Error = fmt.Errorf("failed to form request to storage: %v", err)
40+
return response
41+
}
42+
43+
r.SetQueryParams(map[string]string{
44+
"request": string(requestJSON),
45+
})
46+
47+
resp, err := r.SetDoNotParseResponse(true).Execute("GET", path)
48+
if err != nil {
49+
response.Error = fmt.Errorf("failed to send request: %v", err)
50+
return response
51+
}
52+
defer resp.RawBody().Close()
53+
54+
if resp.StatusCode() != http.StatusOK {
55+
if resp.StatusCode() == http.StatusNotFound {
56+
response.Error = ErrStorageFileNotFound
57+
} else {
58+
response.Error = fmt.Errorf("get request failed with status: %v", resp.Status())
59+
}
60+
return response
61+
}
62+
63+
var filename string
64+
if request.DownloadFilename != nil && *request.DownloadFilename != "" {
65+
filename = *request.DownloadFilename
66+
} else {
67+
// Extract filename from Content-Disposition header
68+
contentDisposition := resp.Header().Get("Content-Disposition")
69+
if contentDisposition != "" {
70+
_, params, err := mime.ParseMediaType(contentDisposition)
71+
if err == nil && params["filename"] != "" {
72+
filename = params["filename"]
73+
}
74+
}
75+
}
76+
77+
if filename == "" {
78+
response.Error = fmt.Errorf("can't extract filename from DownloadFilename or Content-Disposition header")
79+
return response
80+
}
81+
82+
filePath := filepath.Join(request.BaseFolder, filename)
83+
file, err := os.Create(filePath)
84+
if err != nil {
85+
response.Error = fmt.Errorf("failed to create file: %v", err)
86+
return response
87+
}
88+
defer file.Close()
89+
90+
written, err := io.Copy(file, resp.RawBody())
91+
if err != nil {
92+
response.Error = fmt.Errorf("failed to write to file: %v", err)
93+
return response
94+
}
95+
96+
response.Filename = filename
97+
response.BaseFolder = request.BaseFolder
98+
response.Size = uint64(written)
99+
return response
17100
}
18101

19102
func (s *Connector) Upload(request *Request) *Response {
20-
return nil
103+
response := &Response{R: *request}
104+
105+
if request.File == nil {
106+
response.Error = fmt.Errorf("file for upload is not specified")
107+
return response
108+
}
109+
110+
path := "/storage/upload"
111+
r := s.connection.R()
112+
113+
requestJSON, err := json.Marshal(request)
114+
if err != nil {
115+
response.Error = fmt.Errorf("failed to form request to storage: %v", err)
116+
return response
117+
}
118+
119+
r.SetFormData(map[string]string{
120+
"request": string(requestJSON),
121+
})
122+
123+
// request.StorageFilename can be empty
124+
r.SetFileReader("file", request.StorageFilename, request.File)
125+
126+
resp, err := r.Post(path)
127+
if err != nil {
128+
response.Error = fmt.Errorf("failed to send request: %v", err)
129+
return response
130+
}
131+
132+
if resp.IsError() {
133+
response.Error = fmt.Errorf("upload failed with status: %v", resp.Status())
134+
return response
135+
}
136+
137+
return response
21138
}
22139

23140
func (s *Connector) Delete(request *Request) *Response {
24-
return nil
141+
response := &Response{R: *request}
142+
143+
path := "/storage/remove"
144+
r := s.connection.R()
145+
146+
requestJSON, err := json.Marshal(request)
147+
if err != nil {
148+
response.Error = fmt.Errorf("failed to form request to storage: %v", err)
149+
return response
150+
}
151+
152+
r.SetFormData(map[string]string{
153+
"request": string(requestJSON),
154+
})
155+
156+
resp, err := r.Delete(path)
157+
if err != nil {
158+
response.Error = fmt.Errorf("failed to send request: %v", err)
159+
return response
160+
}
161+
162+
if resp.IsError() {
163+
response.Error = fmt.Errorf("delete failed with status: %v", resp.Status())
164+
return response
165+
}
166+
167+
return response
25168
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package storageconn
2+
3+
import "errors"
4+
5+
var ErrStorageFileNotFound = errors.New("file not found in storage")

common/connectors/storageconn/structs.go

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ type Request struct {
1212
// Should be always specified
1313
Resource resource.Type `json:"resource"`
1414

15+
/*
16+
Resource must always has exactly one of ProblemId and SubmitID
17+
Including, only TestID cannot be specified
18+
ID=0 is considered absent
19+
*/
20+
1521
// If resource is part of problem, ProblemID is used
1622
ProblemID uint64 `json:"problemID"`
1723
// If resource is part of submit, SubmitID is used
@@ -22,46 +28,57 @@ type Request struct {
2228
// For any download, BaseFolder should be specified. The files with original filenames will be placed there
2329
BaseFolder string `json:"-"`
2430

25-
// For uploads, Files should be specified. Filename is key in map, file data should be read from value
26-
Files map[string]io.Reader `json:"-"`
31+
// Specify a custom filename for the downloaded file
32+
DownloadFilename *string `json:"-"`
33+
34+
// For uploads, File should be specified
35+
File io.Reader `json:"-"`
36+
37+
// If StorageFilename is not specified, Storage tries to get the filename automatically
38+
StorageFilename string `json:"storageFilename"`
2739
}
2840

2941
type Response struct {
3042
R Request
3143
Error error
3244
}
3345

34-
type ResponseFiles struct {
46+
type FileResponse struct {
3547
Response
36-
fileNames []string
37-
Size uint64
48+
Filename string `json:"filename"`
49+
BaseFolder string `json:"basefolder"`
50+
Size uint64 `json:"size"`
3851
}
3952

40-
func (r *ResponseFiles) File() (string, bool) {
41-
if len(r.fileNames) == 0 {
42-
return "", false
53+
func NewFileResponse(request Request) *FileResponse {
54+
return &FileResponse{
55+
Response: Response{R: request, Error: nil},
56+
Filename: "",
57+
BaseFolder: "",
58+
Size: 0,
4359
}
44-
return filepath.Join(r.R.BaseFolder, r.fileNames[0]), true
4560
}
4661

47-
func (r *ResponseFiles) Get(fileName string) (string, bool) {
48-
for _, f := range r.fileNames {
49-
if fileName == f {
50-
return filepath.Join(r.R.BaseFolder, f), true
51-
}
62+
func (r *FileResponse) GetFilePath() (string, bool) {
63+
if r.BaseFolder == "" || r.Filename == "" {
64+
return "", false
5265
}
53-
return "", false
66+
return filepath.Join(r.BaseFolder, r.Filename), true
5467
}
5568

56-
func (r *ResponseFiles) CleanUp() {
69+
// Removes BaseFolder with all files
70+
func (r *FileResponse) CleanUp() {
5771
if r.Error != nil {
72+
logger.Error("CleanUp was called for failed FileResponse: %v", r.Error)
5873
return
5974
}
60-
if len(r.R.BaseFolder) == 0 {
75+
if r.BaseFolder == "" {
76+
logger.Error("CleanUp was called for empty BaseFolder name")
6177
return
6278
}
63-
err := os.RemoveAll(r.R.BaseFolder)
79+
80+
err := os.RemoveAll(r.BaseFolder)
6481
if err != nil {
65-
logger.Panic("Can not remove resource folder, error: %s", err.Error())
82+
logger.Error("Cannot remove resource folder %s: %s", r.BaseFolder, err.Error())
6683
}
6784
}

common/constants/resource/datatype_string.go

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/constants/resource/resource_type.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Type
2+
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=DataType
23

34
package resource
45

@@ -16,4 +17,14 @@ const (
1617
CheckerOutput
1718
Interactor
1819
// Will be increased
20+
// Don't forget to add a new type to storage/filesystem/resource_info.go
21+
)
22+
23+
type DataType int
24+
25+
const (
26+
UnknownDataType DataType = iota
27+
Problem
28+
Submission
29+
// Will be increased
1930
)

docs/storage.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Устройство Storage
2+
3+
## API-интерфейс
4+
5+
Storage предоставляет три основных метода API:
6+
- `POST /storage/upload` — загрузка файла
7+
- `GET /storage/get` — получение файла
8+
- `DELETE /storage/remove` — удаление файла
9+
10+
Все API-методы принимают следующие параметры:
11+
- `id` — уникальный идентификатор объекта (например, problem ID или submission ID)
12+
- `dataType` — тип данных (например, `problem` или `submission`)
13+
- `filepath` — путь к файлу или имя файла
14+
15+
## Filesystem
16+
17+
Filesystem представляет собой интерфейс для работы с файловой системой и имеет следующие методы:
18+
- `SaveFile` — сохранение загруженного файла
19+
- `RemoveFile` — удаление файла
20+
- `GetFilePath` — получение пути к файлу
21+
22+
## StorageConnector
23+
24+
Для взаимодействия с Storage из других сервисов используется `StorageConn`, который предоставляет следующие методы:
25+
- `Download` — загрузка файла из Storage
26+
- `Upload` — отправка файла в Storage
27+
- `Delete` — удаление файла из Storage

invoker/check_pipeline.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"golang.org/x/net/html/charset"
8-
"io"
98
"os"
109
"path/filepath"
1110
"strings"
@@ -178,9 +177,7 @@ func (s *JobPipelineState) uploadCheckerOutput() error {
178177
Resource: resource.CheckerOutput,
179178
SubmitID: uint64(s.job.Submission.ID),
180179
TestID: s.job.Test,
181-
Files: map[string]io.Reader{
182-
checkOutputFile: s.test.checkerOutputReader,
183-
},
180+
File: s.test.checkerOutputReader,
184181
}
185182
resp := s.invoker.TS.StorageConn.Upload(checkerOutputRequest)
186183
if resp.Error != nil {

0 commit comments

Comments
 (0)