-
Notifications
You must be signed in to change notification settings - Fork 0
Dev/storage #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dev/storage #8
Conversation
c7a4bbf to
5bff50e
Compare
grphil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ещё из общего, постарайся сообщения об ошибках поподробнее писать, а то пока по ним не всегда можно понять что конкретно не так пошло со всеми подробностями
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- go.mod: Language not supported
grphil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ну у меня тут главное замечание по тому, что формат запроса к стораджу довольно странный, после того, как это финализируем, досмотрю ревью
5e3a5cc to
49b4d2e
Compare
grphil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Глобально довольно неплохо, но мне очень не нравится что ты просишь имя файла указать, написал в комментах как это поправить можно
storage/helpers.go
Outdated
| if request.TestID == 0 { | ||
| return "", errors.New("TestID is not specified for test resource") | ||
| } | ||
| return fmt.Sprintf("tests/%s/%s", strconv.FormatUint(request.TestID, 10), request.StorageFilename), nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А, я понял зачем тебе StorageFilename. Смотри, мне кажется проще будет захардкодить определенное имя для разных типов файлов, чтобы всем было проще пользоваться стораджем.
- SourceCode это
source/%sс именем файла (лучше имя сорса сохранить) - CompiledBinary это
soultion - CompileOutput это
compile.out - TestInput это
tests/%02dс номером теста (тут общепринятое название с полигона) - TestAnswer это
tests/%02d.aс номером теста (тут общепринятое название с полигона) - TestOutput это
tests/%02d.outс номером теста - TestStderr это
tests/%02d.errc номером теста - CheckerOutput это
tests/%02d.checkс номером теста - Checker это
check - Interactor это
interactor
А дальше можно делать так:
При загрузке файла у тебя кастомное имя просится только для сорса, а остальных случаях ты говоришь что не надо мне кастомных имен.
При скачивании файла если кастомное имя захардкожено, то берешь его, иначе смотришь на папку, содержащую файл - если там единственный файл, выдаешь его, иначе падаешь с ошибкой и просьбой подробнее указать имя файла
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Я долго над этим думал, и решил сделать везде кастомное имя, потому что в полигоне например checker/validator/generator можно как угодно называть. Но если хочешь, давай докостыляю...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ну можно checker, validator, generator, interactor в отдельные папочки поместить и как сорс брать (если в папке один файл, значит это он, иначе падаем с ошибкой)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Я не стал добавлять логику с проверкой что файл единственный в папке. Там и так код не очень, не очень хочется делать разную логику составления имени файла при upload и get)
storage/helpers.go
Outdated
| } | ||
| } | ||
|
|
||
| func getFilepath(request *storageconn.Request) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Есть идеи как это написать лучше?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут красиво вряд ли получится написать, но я бы так вообще сделал бы: в getInfoFromRequest ты создаешь тип resourceInfo, сохраняя туда request. А дальше для resourceInfo делаешь методы, которые заполняют его поля (вместо текущих функций которые строки возвращают).
Соответственно у тебя получаются методы:
getDataTypegetDataIDgetFilepath
Теперь про getFilepath, тут можно разделить на 2 части: папка (getFilepathFolder) и название файла (parseFilepathFilename)
parseFilepathFolder - ты делаешь стандартный switch, в котором в куче случаев просто не делаешь ничего, а для особых случаев (тест, исходник решения, чекер, валидатор) в поле filepath пишешь имя папки.
parseFilepathFilename - тут заводишь переменную filename в случае фиксированных имен файлов, записываешь его. Если же тип ресурса позволяет указывать Filename, то в одном и том же case для них всех ты чекаешь, указан ли Filename. Если указан, дописываешь его, если не указан, то в отдельное поле внутри структуры resourceInfo указываешь, что файл надо найти внутри папки. А дальше этим поиском пусть filesystem занимается, где все будет консистентно. Дальше сответственно если folder не пустая, дописываешь к ней имя файла, иначе ставишь его сразу на место filepath.
Ещё кстати парсинг тестов можно как нибудь удобно объединить, типа там сделать отдельный case на них всех, проверить что Test есть, и потом в какой нибудь отдельной функции построить суффикс уже четким switch по всем видам тестов
При таком подходе количество switch case будет ещё больше, но зато минимизируется копипаста внутри них
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если указан, дописываешь его, если не указан, то в отдельное поле внутри структуры resourceInfo указываешь, что файл надо найти внутри папки. - Я всё-таки оставил логику, что в таком случае берётся файл с дефолтным названием (которое есть для любого типа). У меня нет понимания почему это плохо (ну только если ты боишься что не будешь его указывать когда надо, и при этом в папке несколько файлов), но если хочешь, то могу дописать.
storage/helpers.go
Outdated
| } | ||
| } | ||
|
|
||
| func getFilepath(request *storageconn.Request) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут красиво вряд ли получится написать, но я бы так вообще сделал бы: в getInfoFromRequest ты создаешь тип resourceInfo, сохраняя туда request. А дальше для resourceInfo делаешь методы, которые заполняют его поля (вместо текущих функций которые строки возвращают).
Соответственно у тебя получаются методы:
getDataTypegetDataIDgetFilepath
Теперь про getFilepath, тут можно разделить на 2 части: папка (getFilepathFolder) и название файла (parseFilepathFilename)
parseFilepathFolder - ты делаешь стандартный switch, в котором в куче случаев просто не делаешь ничего, а для особых случаев (тест, исходник решения, чекер, валидатор) в поле filepath пишешь имя папки.
parseFilepathFilename - тут заводишь переменную filename в случае фиксированных имен файлов, записываешь его. Если же тип ресурса позволяет указывать Filename, то в одном и том же case для них всех ты чекаешь, указан ли Filename. Если указан, дописываешь его, если не указан, то в отдельное поле внутри структуры resourceInfo указываешь, что файл надо найти внутри папки. А дальше этим поиском пусть filesystem занимается, где все будет консистентно. Дальше сответственно если folder не пустая, дописываешь к ней имя файла, иначе ставишь его сразу на место filepath.
Ещё кстати парсинг тестов можно как нибудь удобно объединить, типа там сделать отдельный case на них всех, проверить что Test есть, и потом в какой нибудь отдельной функции построить суффикс уже четким switch по всем видам тестов
При таком подходе количество switch case будет ещё больше, но зато минимизируется копипаста внутри них
storage/helpers.go
Outdated
| return &req, nil | ||
| } | ||
|
|
||
| func parseDataType(resourseInfo *filesystem.ResourceInfo) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А может все функции вида parse* просто в методы ResourceInfo перенести? Зачем им отдельно лежать?
storage/helpers.go
Outdated
| case resource.CompiledBinary, resource.CompileOutput, resource.CheckerOutput: | ||
| return "", nil | ||
| case resource.SourceCode: | ||
| return "sources", nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Вообще хз, зачем эту папку во множественном числе указывать. А ещё в идеале все такие имена в константы в одном месте вынести
storage/helpers.go
Outdated
| return "", errors.New("TestID is not specified for test resource") | ||
| } | ||
| return getTestResourceFilename(request.TestID, request.Resource) | ||
| default: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А как же заифать что Filename нет для source, checker и interactor...
storage/helpers.go
Outdated
| request := resourseInfo.Request | ||
|
|
||
| switch request.Resource { | ||
| case resource.CompiledBinary, resource.CompileOutput, resource.CheckerOutput: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Так, а CheckerOutput это же ресурс для всех тестов. Он по сути как аутпут на тесте должен храниться, он же не общий для всей посылки
| return nil | ||
| func (s *Connector) Download(request *Request) *FileResponse { | ||
| response := NewFileResponse(*request) | ||
| if request.StorageFilename == "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Теперь у тебя вообще очень странно поддержан Filename, в половине случаев он есть, в половине нет, ты всегда требуешь его...
| } | ||
|
|
||
| if resp.IsError() { | ||
| response.Error = fmt.Errorf("get request failed with status: %v", resp.Status()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
О, а давай ещё как-то нормально захендлим, что файла не существует. Какую-то специальную ошибку можем например в этом случае возвращать
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Вынес это в отдельный 404
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А где? Просто текст писать не оч, это парсить больно. Лучше сделай отдельную ошибку ErrFileNotFound и если статус 404, то возвращай ее. Можешь посмотреть как в lib/cache это сделано, там тоже кастомные ошибки есть
b702cc5 to
0ffa07f
Compare
grphil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ну пока тут баги есть
storage/filesystem/filesystem.go
Outdated
| } | ||
|
|
||
| if resourceInfo.EmptyStorageFilename { | ||
| if len(entries) > 1 || (len(entries) == 1 && entries[0] != filepath.Base(resourceInfo.Filepath)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А ты же тут все ещё будешь пытаться достать именно определенное имя, которое дефолтно забрал из FilepathFolderMapping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тут же такая логика: если нам не передали имя, то достаём из папки дефолтный если есть. Или ты хочешь доставать один всегда c любым названием (если не передали)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Да. Вообще кажется лучше разделить на 2 типа все ресурсы: с фиксированным именем, и нефиксированным именем. Соответственно для ресурсов с фиксированным именем мы юзаем только его. Для ресурсов без фиксированного имени мы или пытаемся по filename найти (если есть), или пытаемся просто взять в папке (если такой один)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А всё-таки сохранять ресурсы с нефиксированным именем мы разрешаем, если StorageFilename не указан (с каким-то дефолтным)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Давай запретим. Но оно скорее всего само запретится - в папке ничего не будет, поэтому и не сохранится
storage/filesystem/resource_info.go
Outdated
| resourseInfo.EmptyStorageFilename = true | ||
|
|
||
| filepathFilename, ok := resource.FilepathFilenameMapping[request.Resource] | ||
| if !ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Мне кажется лучше не добавлять дефолтное имя для тех файлов, которые не имеют StorageFilename, и как раз !ok тут будет индикатором того, что имя файла надо вычислять самому. И как раз только в этом случае будет осмысленно resourseInfo.EmptyStorageFilename делать равным true
Кстати у тебя сейчас есть ещё одна бага - ты для всех видов файлов указываешь EmptyStorageFilename, даже даже если у них есть дефолтное имя в сторадже (например тесты). И когда ты их берешь, то папка с ними содержит ещё файлы и ты кидаешь ошибку
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Так здесь маппинг говорит дефолтный filename для ресурса, если нет StorageFilename. Ты типо хочешь, чтобы на уровне ResourceInfo было 2 варианта filename: есть переданный StorageFilename и нет?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Написал в другом комменте:
Вообще кажется лучше разделить на 2 типа все ресурсы: с фиксированным именем, и нефиксированным именем. Соответственно для ресурсов с фиксированным именем мы юзаем только его. Для ресурсов без фиксированного имени мы или пытаемся по filename найти (если есть), или пытаемся просто взять в папке (если такой один)
storage/filesystem/filesystem.go
Outdated
| } | ||
|
|
||
| dir := filepath.Dir(fullPath) | ||
| entries, err := filesystem.GetDirEntriesNames(dir) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Мне кажется все такие штуки лучше вызвать именно на этапе BuildFilePath, типа просто там чекаешь - если resourceInfo.EmptyStorageFilename = false, то берешь resourceInfo.Filepath как путь к файлу, иначе берешь resourceInfo.Filepath как путь к папке и дальше берешь все содержимое папки и там ищешь файл.
storage/filesystem/filesystem.go
Outdated
| } | ||
|
|
||
| func (filesystem *Filesystem) BuildFilePath(resourceInfo *ResourceInfo) (string, error) { | ||
| cleanFilename := filepath.Base(resourceInfo.Filepath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ты же тут теряешь полностью весь путь до resourceInfo.Filepath со всеми этими папочками
storage/filesystem/filesystem.go
Outdated
| subpathID := filesystem.generatePathFromID(resourceInfo.DataType.String(), strconv.FormatUint(resourceInfo.ID, 10)) | ||
| fullPath := filepath.Join(filesystem.Basepath, subpathID, cleanFilename) | ||
|
|
||
| absBasepath, err := filepath.Abs(filesystem.Basepath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А зачем тебе вообще все эти чеки с absBasepath и absFullPath?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Чтобы проверить, что мы случайно за пределы корня файловой системы не ушли (если какие-нибудь ../ случайно передали в StorageFilename).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ну видимо действительно уберу всё это
| } | ||
|
|
||
| if resp.IsError() { | ||
| response.Error = fmt.Errorf("get request failed with status: %v", resp.Status()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А где? Просто текст писать не оч, это парсить больно. Лучше сделай отдельную ошибку ErrFileNotFound и если статус 404, то возвращай ее. Можешь посмотреть как в lib/cache это сделано, там тоже кастомные ошибки есть
grphil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ну сейчас тут пара фиксов осталось сделать
storage/handlers.go
Outdated
|
|
||
| err = s.filesystem.GetFile(resourceInfo) | ||
| if err != nil { | ||
| logger.Error("Failed to get file: id=%d, dataType=%s, filePath=%s\n %v", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Давай ошибку будем в лог писать если это не os.NotExistErr, а то если файла нет, то у нас ошибки то не произошло, просто запрос плохой. То есть перенести на 4 строчки вниз сообщение
storage/filesystem/filesystem.go
Outdated
| return "", fmt.Errorf("StorageFilename is not specified, but directory %s does not contain exactly one file", fullPath) | ||
| } | ||
| fullPath = filepath.Join(fullPath, entries[0].Name()) | ||
| resourceInfo.Filepath = fullPath // It lets use resourceInfo.Filepath after successful SaveFile, RemoveFile and GetFile calls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Смотри, тут несколько проблем:
Во первых, ты в половине случаев обновляешь Filepath, а в половине случаев нет. У тебя кстати и download вроде не будет работать если не было EmptyStorageFilename.
Во вторых, в целом мне кажется не оч хорошо менять resourceInfo, оно же как бы константно нам передается. Мне кажется лучше только возвращать именно строку с полным путем до файла
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filepath всегда будет заполнен (при корректном вызове), потому что мы его в resouce_info.go заполняем ещё (и там если не EmptyStorageFilename, то мы полный путь запишем), поэтому Download кажется должен работать.
Вообще я эту штуку в последний момент поменял) мне очень захотелось сделать одинаковую сигнатуру всех методов filesystem + не писать путь в двух местах
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filepath всегда будет заполнен (при корректном вызове), потому что мы его в resouce_info.go заполняем ещё (и там если не EmptyStorageFilename, то мы полный путь запишем), поэтому Download кажется должен работать.
Не, там у тебя Filepath заполняется как путь внутри пакета задачи/посылки. Но мы не можем его использовать как просто путь до файла, у тебя же куча промежуточных частей
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Согласен, хуйню написал
storage/filesystem/filesystem.go
Outdated
| return nil | ||
| } | ||
|
|
||
| func (filesystem *Filesystem) GetFile(resourceInfo *ResourceInfo) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Мне кажется тут скорее логично именно путь до файла возвращать, а не resourceInfo менять и туда куда-то этот путь проставлять
| ) | ||
|
|
||
| // FilepathFolderMapping should contain all types | ||
| var FilepathFolderMapping = map[Type]string{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А давай эти два мапа в storage перетащим, оно же никакой другой компоненте не нужно
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Я хотел тут их оставить, чтобы точно не забыли. Но там при добавлении типа всё равно скорее всего придётся код в storage дописывать, так что ок
| return response | ||
| } | ||
|
|
||
| filename := "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Лучше пустые типы через var filename string создавать
| } | ||
| defer file.Close() | ||
|
|
||
| _, err = io.Copy(file, bytes.NewReader(resp.Body())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
О, а я вроде уже писал раньше, видимо потерялось. Давай не будем целиком resp.Body() хранить. Оно будет очень сильно заполнять оперативку когда ты грузишь тесты по 100-200мб и там реально может просто оперативки не хватить. Лучше вместо этого в запросе у resty настрой чтобы body не сохранялось, а вместо этого был ридер по нему, и из него все вычитай. (не забудь ещё этот ридер закрыть)
grphil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Только один супермелкий коммент! Ну и порезолви там мердж конфликт
| } | ||
| defer resp.RawBody().Close() | ||
|
|
||
| if resp.StatusCode() >= 400 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Давай лучше лучше resp.StatusCode() == http.StatusOK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Наверно всё-таки !=)
grphil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ладно, держи свой шип!
No description provided.