Skip to content

Commit ac92674

Browse files
committed
feat(storage/import): add support for additional content types and --content-type flag
- Support IANA-registered content types: application/gzip, application/x-xz, application/x-tar, application/x-bzip2, application/x-7z-compressed, application/zip, and application/octet-stream - Add --content-type flag to allow explicit content type specification - Auto-detect content type from file extension with fallback to octet-stream - Add getContentType() function for extensible content type mapping - Add comprehensive unit tests for content type detection Fixes #581
1 parent a2ac26d commit ac92674

File tree

2 files changed

+60
-8
lines changed

2 files changed

+60
-8
lines changed

internal/commands/storage/import.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type importCommand struct {
6060
existingStorageUUIDOrName string
6161
noWait config.OptionalBoolean
6262
wait config.OptionalBoolean
63+
contentType string
6364

6465
createParams createParams
6566

@@ -71,6 +72,7 @@ func (s *importCommand) InitCommand() {
7172
flagSet := &pflag.FlagSet{}
7273
flagSet.StringVar(&s.sourceLocation, "source-location", "", "Location of the source of the import. Can be a file or a URL.")
7374
flagSet.StringVar(&s.existingStorageUUIDOrName, "storage", "", "Import to an existing storage. Storage must be large enough and must be undetached or the server where the storage is attached must be in shutdown state.")
75+
flagSet.StringVar(&s.contentType, "content-type", "", "Content type of the file being imported. If not specified, it will be automatically detected based on file extension. Supported types: application/gzip, application/x-xz, application/x-tar, application/x-bzip2, application/x-7z-compressed, application/zip, application/octet-stream")
7476
config.AddToggleFlag(flagSet, &s.noWait, "no-wait", false, "When importing from remote url, do not wait until the import finishes or storage is in online state. If set, command will exit after import process has been initialized.")
7577
config.AddToggleFlag(flagSet, &s.wait, "wait", false, "Wait for storage to be in online state before returning.")
7678
applyCreateFlags(flagSet, &s.createParams, defaultCreateParams)
@@ -206,7 +208,7 @@ func (s *importCommand) ExecuteWithoutArguments(exec commands.Executor) (output.
206208
if err != nil {
207209
return commands.HandleError(exec, msg, fmt.Errorf("cannot open local file: %w", err))
208210
}
209-
go importLocalFile(exec, storageToImportTo.UUID, sourceFile, statusChan)
211+
go importLocalFile(exec, storageToImportTo.UUID, sourceFile, s.contentType, statusChan)
210212
}
211213

212214
// import has been triggered, read updates from the process
@@ -351,19 +353,42 @@ func pollStorageImportStatus(exec commands.Executor, uuid string, statusChan cha
351353
}
352354
}
353355

354-
func importLocalFile(exec commands.Executor, uuid string, file *os.File, statusChan chan<- storageImportStatus) {
356+
func getContentType(filename string) string {
357+
// Map file extensions to their IANA-registered content types
358+
// Based on UpCloud Storage Import API documentation
359+
contentTypes := map[string]string{
360+
".gz": "application/gzip",
361+
".xz": "application/x-xz",
362+
".iso": "application/octet-stream",
363+
".img": "application/octet-stream",
364+
".raw": "application/octet-stream",
365+
".qcow2": "application/octet-stream",
366+
".tar": "application/x-tar",
367+
".bz2": "application/x-bzip2",
368+
".7z": "application/x-7z-compressed",
369+
".zip": "application/zip",
370+
}
371+
372+
ext := filepath.Ext(filename)
373+
if contentType, exists := contentTypes[ext]; exists {
374+
return contentType
375+
}
376+
377+
// Default to octet-stream for unknown types
378+
return "application/octet-stream"
379+
}
380+
381+
func importLocalFile(exec commands.Executor, uuid string, file *os.File, userContentType string, statusChan chan<- storageImportStatus) {
355382
// make sure we close the channel when exiting import
356383
defer close(statusChan)
357384
chDone := make(chan storageImportStatus)
358385
reader := &readerCounter{source: file}
359386

360387
// figure out content type
361-
contentType := "application/octet-stream"
362-
switch filepath.Ext(file.Name()) {
363-
case ".gz":
364-
contentType = "application/gzip"
365-
case ".xz":
366-
contentType = "application/x-xz"
388+
// use user-provided content type if specified, otherwise auto-detect
389+
contentType := userContentType
390+
if contentType == "" {
391+
contentType = getContentType(file.Name())
367392
}
368393

369394
go func() {

internal/commands/storage/import_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,33 @@ func TestImportCommand(t *testing.T) {
164164
}
165165
}
166166

167+
func TestGetContentType(t *testing.T) {
168+
tests := []struct {
169+
filename string
170+
expected string
171+
}{
172+
{"image.iso", "application/octet-stream"},
173+
{"image.img", "application/octet-stream"},
174+
{"image.raw", "application/octet-stream"},
175+
{"image.qcow2", "application/octet-stream"},
176+
{"archive.gz", "application/gzip"},
177+
{"archive.xz", "application/x-xz"},
178+
{"archive.tar", "application/x-tar"},
179+
{"archive.bz2", "application/x-bzip2"},
180+
{"archive.7z", "application/x-7z-compressed"},
181+
{"archive.zip", "application/zip"},
182+
{"unknown.bin", "application/octet-stream"},
183+
{"noextension", "application/octet-stream"},
184+
}
185+
186+
for _, test := range tests {
187+
t.Run(test.filename, func(t *testing.T) {
188+
result := getContentType(test.filename)
189+
assert.Equal(t, test.expected, result)
190+
})
191+
}
192+
}
193+
167194
func TestParseSource(t *testing.T) {
168195
for _, test := range []struct {
169196
name string

0 commit comments

Comments
 (0)