forked from TrueCloudLab/rclone
drive: rewrite mime type and extension handling
Make use of the mime package to find matching extensions and mime types. For simplicity, all extensions are now prefixed with "." to match the mime package requirements. Parsed extensions get converted if needed.
This commit is contained in:
parent
d9a3b26e47
commit
690a44e40e
2 changed files with 174 additions and 92 deletions
|
@ -13,6 +13,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -68,32 +69,44 @@ var (
|
||||||
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
|
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
|
||||||
RedirectURL: oauthutil.TitleBarRedirectURL,
|
RedirectURL: oauthutil.TitleBarRedirectURL,
|
||||||
}
|
}
|
||||||
mimeTypeToExtension = map[string]string{
|
_mimeTypeToExtensionDuplicates = map[string]string{
|
||||||
"application/epub+zip": "epub",
|
"application/x-vnd.oasis.opendocument.presentation": ".odp",
|
||||||
"application/msword": "doc",
|
"application/x-vnd.oasis.opendocument.spreadsheet": ".ods",
|
||||||
"application/pdf": "pdf",
|
"application/x-vnd.oasis.opendocument.text": ".odt",
|
||||||
"application/rtf": "rtf",
|
"image/jpg": ".jpg",
|
||||||
"application/vnd.ms-excel": "xls",
|
"image/x-bmp": ".bmp",
|
||||||
"application/vnd.oasis.opendocument.presentation": "odp",
|
"image/x-png": ".png",
|
||||||
"application/vnd.oasis.opendocument.spreadsheet": "ods",
|
"text/rtf": ".rtf",
|
||||||
"application/vnd.oasis.opendocument.text": "odt",
|
}
|
||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
_mimeTypeToExtension = map[string]string{
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
"application/epub+zip": ".epub",
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
"application/json": ".json",
|
||||||
"application/x-vnd.oasis.opendocument.spreadsheet": "ods",
|
"application/msword": ".doc",
|
||||||
"application/zip": "zip",
|
"application/pdf": ".pdf",
|
||||||
"image/jpeg": "jpg",
|
"application/rtf": ".rtf",
|
||||||
"image/png": "png",
|
"application/vnd.ms-excel": ".xls",
|
||||||
"image/svg+xml": "svg",
|
"application/vnd.oasis.opendocument.presentation": ".odp",
|
||||||
"text/csv": "csv",
|
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
||||||
"text/html": "html",
|
"application/vnd.oasis.opendocument.text": ".odt",
|
||||||
"text/plain": "txt",
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
||||||
"text/tab-separated-values": "tsv",
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||||
|
"application/x-msmetafile": ".wmf",
|
||||||
|
"application/zip": ".zip",
|
||||||
|
"image/bmp": ".bmp",
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/pjpeg": ".pjpeg",
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/svg+xml": ".svg",
|
||||||
|
"text/csv": ".csv",
|
||||||
|
"text/html": ".html",
|
||||||
|
"text/plain": ".txt",
|
||||||
|
"text/tab-separated-values": ".tsv",
|
||||||
}
|
}
|
||||||
extensionToMimeType map[string]string
|
extensionToMimeType map[string]string
|
||||||
partialFields = "id,name,size,md5Checksum,trashed,modifiedTime,createdTime,mimeType,parents"
|
partialFields = "id,name,size,md5Checksum,trashed,modifiedTime,createdTime,mimeType,parents"
|
||||||
exportFormatsOnce sync.Once // make sure we fetch the export formats only once
|
exportFormatsOnce sync.Once // make sure we fetch the export formats only once
|
||||||
_exportFormats map[string][]string // allowed export mime-type conversions
|
_exportFormats map[string][]string // allowed export MIME type conversions
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register with Fs
|
// Register with Fs
|
||||||
|
@ -252,10 +265,15 @@ func init() {
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Invert mimeTypeToExtension
|
// register duplicate MIME types first
|
||||||
extensionToMimeType = make(map[string]string, len(mimeTypeToExtension))
|
// this allows them to be used with mime.ExtensionsByType() but
|
||||||
for mimeType, extension := range mimeTypeToExtension {
|
// mime.TypeByExtension() will return the later registered MIME type
|
||||||
extensionToMimeType[extension] = mimeType
|
for _, m := range []map[string]string{_mimeTypeToExtensionDuplicates, _mimeTypeToExtension} {
|
||||||
|
for mimeType, extension := range m {
|
||||||
|
if err := mime.AddExtensionType(extension, mimeType); err != nil {
|
||||||
|
log.Fatalf("Failed to register MIME type %q: %v", mimeType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,7 +444,7 @@ func (f *Fs) list(dirIDs []string, title string, directoriesOnly bool, filesOnly
|
||||||
// if the search title contains an extension and the extension is in the export extensions add a search
|
// if the search title contains an extension and the extension is in the export extensions add a search
|
||||||
// for the filename without the extension.
|
// for the filename without the extension.
|
||||||
// assume that export extensions don't contain escape sequences and only have one part (not .tar.gz)
|
// assume that export extensions don't contain escape sequences and only have one part (not .tar.gz)
|
||||||
if ext := path.Ext(searchTitle); handleGdocs && len(ext) > 0 && containsString(f.extensions, ext[1:]) {
|
if ext := path.Ext(searchTitle); handleGdocs && len(ext) > 0 && containsString(f.extensions, ext) {
|
||||||
stem = title[:len(title)-len(ext)]
|
stem = title[:len(title)-len(ext)]
|
||||||
query = append(query, fmt.Sprintf("(name='%s' or name='%s')", searchTitle, searchTitle[:len(searchTitle)-len(ext)]))
|
query = append(query, fmt.Sprintf("(name='%s' or name='%s')", searchTitle, searchTitle[:len(searchTitle)-len(ext)]))
|
||||||
} else {
|
} else {
|
||||||
|
@ -516,25 +534,78 @@ func isPowerOfTwo(x int64) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseExtensions parses drive export extensions from a string
|
// add a charset parameter to all text/* MIME types
|
||||||
func (f *Fs) parseExtensions(extensions string) error {
|
func fixMimeType(mimeType string) string {
|
||||||
for _, extension := range strings.Split(extensions, ",") {
|
mediaType, param, err := mime.ParseMediaType(mimeType)
|
||||||
extension = strings.ToLower(strings.TrimSpace(extension))
|
if err != nil {
|
||||||
if _, found := extensionToMimeType[extension]; !found {
|
return mimeType
|
||||||
return errors.Errorf("couldn't find mime type for extension %q", extension)
|
}
|
||||||
}
|
if strings.HasPrefix(mimeType, "text/") && param["charset"] == "" {
|
||||||
found := false
|
param["charset"] = "utf-8"
|
||||||
for _, existingExtension := range f.extensions {
|
mimeType = mime.FormatMediaType(mediaType, param)
|
||||||
if extension == existingExtension {
|
}
|
||||||
found = true
|
return mimeType
|
||||||
break
|
}
|
||||||
|
func fixMimeTypeMap(m map[string][]string) map[string][]string {
|
||||||
|
for _, v := range m {
|
||||||
|
for i, mt := range v {
|
||||||
|
fixed := fixMimeType(mt)
|
||||||
|
if fixed == "" {
|
||||||
|
panic(errors.Errorf("unable to fix MIME type %q", mt))
|
||||||
}
|
}
|
||||||
}
|
v[i] = fixed
|
||||||
if !found {
|
|
||||||
f.extensions = append(f.extensions, extension)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return m
|
||||||
|
}
|
||||||
|
func isInternalMimeType(mimeType string) bool {
|
||||||
|
return strings.HasPrefix(mimeType, "application/vnd.google-apps.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExtensions parses a list of comma separated extensions
|
||||||
|
// into a list of unique extensions with leading "."
|
||||||
|
func parseExtensions(extensions ...string) ([]string, error) {
|
||||||
|
var result []string
|
||||||
|
for _, extensionText := range extensions {
|
||||||
|
for _, extension := range strings.Split(extensionText, ",") {
|
||||||
|
extension = strings.ToLower(strings.TrimSpace(extension))
|
||||||
|
if len(extension) > 0 && extension[0] != '.' {
|
||||||
|
extension = "." + extension
|
||||||
|
}
|
||||||
|
if mime.TypeByExtension(extension) == "" {
|
||||||
|
return result, errors.Errorf("couldn't find MIME type for extension %q", extension)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, existingExtension := range result {
|
||||||
|
if extension == existingExtension {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
result = append(result, extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExtensionMimeTypes parses the given extensions using parseExtensions
|
||||||
|
// and maps each resulting extension to its MIME type.
|
||||||
|
func parseExtensionMimeTypes(extensions ...string) ([]string, error) {
|
||||||
|
parsedExtensions, err := parseExtensions(extensions...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mimeTypes := make([]string, 0, len(parsedExtensions))
|
||||||
|
for i, extension := range parsedExtensions {
|
||||||
|
mt := mime.TypeByExtension(extension)
|
||||||
|
if mt == "" {
|
||||||
|
return nil, errors.Errorf("couldn't find MIME type for extension %q", extension)
|
||||||
|
}
|
||||||
|
mimeTypes[i] = mt
|
||||||
|
}
|
||||||
|
return mimeTypes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out if the user wants to use a team drive
|
// Figure out if the user wants to use a team drive
|
||||||
|
@ -699,11 +770,7 @@ func NewFs(name, path string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
f.dirCache = dircache.New(root, f.rootFolderID, f)
|
f.dirCache = dircache.New(root, f.rootFolderID, f)
|
||||||
|
|
||||||
// Parse extensions
|
// Parse extensions
|
||||||
err = f.parseExtensions(opt.Extensions)
|
f.extensions, err = parseExtensions(opt.Extensions, defaultExtensions)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = f.parseExtensions(defaultExtensions) // make sure there are some sensible ones on there
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -836,12 +903,12 @@ func (f *Fs) exportFormats() map[string][]string {
|
||||||
_exportFormats = map[string][]string{}
|
_exportFormats = map[string][]string{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_exportFormats = about.ExportFormats
|
_exportFormats = fixMimeTypeMap(about.ExportFormats)
|
||||||
})
|
})
|
||||||
return _exportFormats
|
return _exportFormats
|
||||||
}
|
}
|
||||||
|
|
||||||
// findExportFormat works out the optimum extension and mime-type
|
// findExportFormat works out the optimum extension and MIME type
|
||||||
// for this item.
|
// for this item.
|
||||||
//
|
//
|
||||||
// Look through the extensions and find the first format that can be
|
// Look through the extensions and find the first format that can be
|
||||||
|
@ -849,11 +916,11 @@ func (f *Fs) exportFormats() map[string][]string {
|
||||||
func (f *Fs) findExportFormat(item *drive.File) (extension, filename, mimeType string, isDocument bool) {
|
func (f *Fs) findExportFormat(item *drive.File) (extension, filename, mimeType string, isDocument bool) {
|
||||||
exportMimeTypes, isDocument := f.exportFormats()[item.MimeType]
|
exportMimeTypes, isDocument := f.exportFormats()[item.MimeType]
|
||||||
if isDocument {
|
if isDocument {
|
||||||
for _, extension := range f.extensions {
|
for _, _extension := range f.extensions {
|
||||||
mimeType := extensionToMimeType[extension]
|
_mimeType := mime.TypeByExtension(_extension)
|
||||||
for _, emt := range exportMimeTypes {
|
for _, emt := range exportMimeTypes {
|
||||||
if emt == mimeType {
|
if emt == _mimeType {
|
||||||
return extension, fmt.Sprintf("%s.%s", item.Name, extension), mimeType, true
|
return _extension, item.Name + _extension, _mimeType, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1103,27 +1170,11 @@ func (f *Fs) itemToDirEntry(remote string, item *drive.File) (fs.DirEntry, error
|
||||||
fs.Debugf(remote, "No export formats found for %q", item.MimeType)
|
fs.Debugf(remote, "No export formats found for %q", item.MimeType)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
o, err := f.newObjectWithInfo(remote+"."+extension, item)
|
o, err := f.newObjectWithInfo(remote+extension, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
obj := o.(*Object)
|
o.(*Object).setGdocsMetaData(item, extension, exportMimeType)
|
||||||
obj.url = fmt.Sprintf("%sfiles/%s/export?mimeType=%s", f.svc.BasePath, item.Id, url.QueryEscape(exportMimeType))
|
|
||||||
if f.opt.AlternateExport {
|
|
||||||
switch item.MimeType {
|
|
||||||
case "application/vnd.google-apps.drawing":
|
|
||||||
obj.url = fmt.Sprintf("https://docs.google.com/drawings/d/%s/export/%s", item.Id, extension)
|
|
||||||
case "application/vnd.google-apps.document":
|
|
||||||
obj.url = fmt.Sprintf("https://docs.google.com/document/d/%s/export?format=%s", item.Id, extension)
|
|
||||||
case "application/vnd.google-apps.spreadsheet":
|
|
||||||
obj.url = fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/export?format=%s", item.Id, extension)
|
|
||||||
case "application/vnd.google-apps.presentation":
|
|
||||||
obj.url = fmt.Sprintf("https://docs.google.com/presentation/d/%s/export/%s", item.Id, extension)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
obj.isDocument = true
|
|
||||||
obj.mimeType = exportMimeType
|
|
||||||
obj.bytes = -1
|
|
||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -1866,13 +1917,13 @@ func (o *Object) setGdocsMetaData(info *drive.File, extension, exportMimeType st
|
||||||
if o.fs.opt.AlternateExport {
|
if o.fs.opt.AlternateExport {
|
||||||
switch info.MimeType {
|
switch info.MimeType {
|
||||||
case "application/vnd.google-apps.drawing":
|
case "application/vnd.google-apps.drawing":
|
||||||
o.url = fmt.Sprintf("https://docs.google.com/drawings/d/%s/export/%s", info.Id, extension)
|
o.url = fmt.Sprintf("https://docs.google.com/drawings/d/%s/export/%s", info.Id, extension[1:])
|
||||||
case "application/vnd.google-apps.document":
|
case "application/vnd.google-apps.document":
|
||||||
o.url = fmt.Sprintf("https://docs.google.com/document/d/%s/export?format=%s", info.Id, extension)
|
o.url = fmt.Sprintf("https://docs.google.com/document/d/%s/export?format=%s", info.Id, extension[1:])
|
||||||
case "application/vnd.google-apps.spreadsheet":
|
case "application/vnd.google-apps.spreadsheet":
|
||||||
o.url = fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/export?format=%s", info.Id, extension)
|
o.url = fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/export?format=%s", info.Id, extension[1:])
|
||||||
case "application/vnd.google-apps.presentation":
|
case "application/vnd.google-apps.presentation":
|
||||||
o.url = fmt.Sprintf("https://docs.google.com/presentation/d/%s/export/%s", info.Id, extension)
|
o.url = fmt.Sprintf("https://docs.google.com/presentation/d/%s/export/%s", info.Id, extension[1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
o.isDocument = true
|
o.isDocument = true
|
||||||
|
|
|
@ -2,6 +2,7 @@ package drive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"mime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"google.golang.org/api/drive/v3"
|
"google.golang.org/api/drive/v3"
|
||||||
|
@ -57,6 +58,7 @@ const exampleExportFormats = `{
|
||||||
func TestInternalLoadExampleExportFormats(t *testing.T) {
|
func TestInternalLoadExampleExportFormats(t *testing.T) {
|
||||||
exportFormatsOnce.Do(func() {})
|
exportFormatsOnce.Do(func() {})
|
||||||
assert.NoError(t, json.Unmarshal([]byte(exampleExportFormats), &_exportFormats))
|
assert.NoError(t, json.Unmarshal([]byte(exampleExportFormats), &_exportFormats))
|
||||||
|
_exportFormats = fixMimeTypeMap(_exportFormats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInternalParseExtensions(t *testing.T) {
|
func TestInternalParseExtensions(t *testing.T) {
|
||||||
|
@ -65,27 +67,24 @@ func TestInternalParseExtensions(t *testing.T) {
|
||||||
want []string
|
want []string
|
||||||
wantErr error
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{"doc", []string{"doc"}, nil},
|
{"doc", []string{".doc"}, nil},
|
||||||
{" docx ,XLSX, pptx,svg", []string{"docx", "xlsx", "pptx", "svg"}, nil},
|
{" docx ,XLSX, pptx,svg", []string{".docx", ".xlsx", ".pptx", ".svg"}, nil},
|
||||||
{"docx,svg,Docx", []string{"docx", "svg"}, nil},
|
{"docx,svg,Docx", []string{".docx", ".svg"}, nil},
|
||||||
{"docx,potato,docx", []string{"docx"}, errors.New(`couldn't find mime type for extension "potato"`)},
|
{"docx,potato,docx", []string{".docx"}, errors.New(`couldn't find MIME type for extension ".potato"`)},
|
||||||
} {
|
} {
|
||||||
f := new(Fs)
|
extensions, gotErr := parseExtensions(test.in)
|
||||||
gotErr := f.parseExtensions(test.in)
|
|
||||||
if test.wantErr == nil {
|
if test.wantErr == nil {
|
||||||
assert.NoError(t, gotErr)
|
assert.NoError(t, gotErr)
|
||||||
} else {
|
} else {
|
||||||
assert.EqualError(t, gotErr, test.wantErr.Error())
|
assert.EqualError(t, gotErr, test.wantErr.Error())
|
||||||
}
|
}
|
||||||
assert.Equal(t, test.want, f.extensions)
|
assert.Equal(t, test.want, extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test it is appending
|
// Test it is appending
|
||||||
f := new(Fs)
|
extensions, gotErr := parseExtensions("docx,svg", "docx,svg,xlsx")
|
||||||
assert.Nil(t, f.parseExtensions("docx,svg"))
|
assert.NoError(t, gotErr)
|
||||||
assert.Nil(t, f.parseExtensions("docx,svg,xlsx"))
|
assert.Equal(t, []string{".docx", ".svg", ".xlsx"}, extensions)
|
||||||
assert.Equal(t, []string{"docx", "svg", "xlsx"}, f.extensions)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInternalFindExportFormat(t *testing.T) {
|
func TestInternalFindExportFormat(t *testing.T) {
|
||||||
|
@ -99,10 +98,10 @@ func TestInternalFindExportFormat(t *testing.T) {
|
||||||
wantMimeType string
|
wantMimeType string
|
||||||
}{
|
}{
|
||||||
{[]string{}, "", ""},
|
{[]string{}, "", ""},
|
||||||
{[]string{"pdf"}, "pdf", "application/pdf"},
|
{[]string{".pdf"}, ".pdf", "application/pdf"},
|
||||||
{[]string{"pdf", "rtf", "xls"}, "pdf", "application/pdf"},
|
{[]string{".pdf", ".rtf", ".xls"}, ".pdf", "application/pdf"},
|
||||||
{[]string{"xls", "rtf", "pdf"}, "rtf", "application/rtf"},
|
{[]string{".xls", ".rtf", ".pdf"}, ".rtf", "application/rtf"},
|
||||||
{[]string{"xls", "csv", "svg"}, "", ""},
|
{[]string{".xls", ".csv", ".svg"}, "", ""},
|
||||||
} {
|
} {
|
||||||
f := new(Fs)
|
f := new(Fs)
|
||||||
f.extensions = test.extensions
|
f.extensions = test.extensions
|
||||||
|
@ -117,3 +116,35 @@ func TestInternalFindExportFormat(t *testing.T) {
|
||||||
assert.Equal(t, true, gotIsDocument)
|
assert.Equal(t, true, gotIsDocument)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMimeTypesToExtension(t *testing.T) {
|
||||||
|
for mimeType, extension := range _mimeTypeToExtension {
|
||||||
|
extensions, err := mime.ExtensionsByType(mimeType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, extensions, extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtensionToMimeType(t *testing.T) {
|
||||||
|
for mimeType, extension := range _mimeTypeToExtension {
|
||||||
|
gotMimeType := mime.TypeByExtension(extension)
|
||||||
|
mediatype, _, err := mime.ParseMediaType(gotMimeType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, mimeType, mediatype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtensionsForExportFormats(t *testing.T) {
|
||||||
|
if _exportFormats == nil {
|
||||||
|
t.Error("exportFormats == nil")
|
||||||
|
}
|
||||||
|
for fromMT, toMTs := range _exportFormats {
|
||||||
|
for _, toMT := range toMTs {
|
||||||
|
if !isInternalMimeType(toMT) {
|
||||||
|
extensions, err := mime.ExtensionsByType(toMT)
|
||||||
|
assert.NoError(t, err, "invalid MIME type %q", toMT)
|
||||||
|
assert.NotEmpty(t, extensions, "No extension found for %q (from: %q)", fromMT, toMT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue