David K. Bainbridge | 528b318 | 2017-01-23 08:51:59 -0800 | [diff] [blame^] | 1 | // Copyright 2016 Canonical Ltd. |
| 2 | // Licensed under the LGPLv3, see LICENCE file for details. |
| 3 | |
| 4 | package gomaasapi |
| 5 | |
| 6 | import ( |
| 7 | "encoding/base64" |
| 8 | "net/http" |
| 9 | "net/url" |
| 10 | |
| 11 | "github.com/juju/errors" |
| 12 | "github.com/juju/schema" |
| 13 | "github.com/juju/version" |
| 14 | ) |
| 15 | |
| 16 | type file struct { |
| 17 | controller *controller |
| 18 | |
| 19 | resourceURI string |
| 20 | filename string |
| 21 | anonymousURI *url.URL |
| 22 | content string |
| 23 | } |
| 24 | |
| 25 | // Filename implements File. |
| 26 | func (f *file) Filename() string { |
| 27 | return f.filename |
| 28 | } |
| 29 | |
| 30 | // AnonymousURL implements File. |
| 31 | func (f *file) AnonymousURL() string { |
| 32 | url := f.controller.client.GetURL(f.anonymousURI) |
| 33 | return url.String() |
| 34 | } |
| 35 | |
| 36 | // Delete implements File. |
| 37 | func (f *file) Delete() error { |
| 38 | err := f.controller.delete(f.resourceURI) |
| 39 | if err != nil { |
| 40 | if svrErr, ok := errors.Cause(err).(ServerError); ok { |
| 41 | switch svrErr.StatusCode { |
| 42 | case http.StatusNotFound: |
| 43 | return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) |
| 44 | case http.StatusForbidden: |
| 45 | return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) |
| 46 | } |
| 47 | } |
| 48 | return NewUnexpectedError(err) |
| 49 | } |
| 50 | return nil |
| 51 | } |
| 52 | |
| 53 | // ReadAll implements File. |
| 54 | func (f *file) ReadAll() ([]byte, error) { |
| 55 | if f.content == "" { |
| 56 | return f.readFromServer() |
| 57 | } |
| 58 | bytes, err := base64.StdEncoding.DecodeString(f.content) |
| 59 | if err != nil { |
| 60 | return nil, NewUnexpectedError(err) |
| 61 | } |
| 62 | return bytes, nil |
| 63 | } |
| 64 | |
| 65 | func (f *file) readFromServer() ([]byte, error) { |
| 66 | // If the content is available, it is base64 encoded, so |
| 67 | args := make(url.Values) |
| 68 | args.Add("filename", f.filename) |
| 69 | bytes, err := f.controller._getRaw("files", "get", args) |
| 70 | if err != nil { |
| 71 | if svrErr, ok := errors.Cause(err).(ServerError); ok { |
| 72 | switch svrErr.StatusCode { |
| 73 | case http.StatusNotFound: |
| 74 | return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage)) |
| 75 | case http.StatusForbidden: |
| 76 | return nil, errors.Wrap(err, NewPermissionError(svrErr.BodyMessage)) |
| 77 | } |
| 78 | } |
| 79 | return nil, NewUnexpectedError(err) |
| 80 | } |
| 81 | return bytes, nil |
| 82 | } |
| 83 | |
| 84 | func readFiles(controllerVersion version.Number, source interface{}) ([]*file, error) { |
| 85 | readFunc, err := getFileDeserializationFunc(controllerVersion) |
| 86 | if err != nil { |
| 87 | return nil, errors.Trace(err) |
| 88 | } |
| 89 | |
| 90 | checker := schema.List(schema.StringMap(schema.Any())) |
| 91 | coerced, err := checker.Coerce(source, nil) |
| 92 | if err != nil { |
| 93 | return nil, WrapWithDeserializationError(err, "file base schema check failed") |
| 94 | } |
| 95 | valid := coerced.([]interface{}) |
| 96 | return readFileList(valid, readFunc) |
| 97 | } |
| 98 | |
| 99 | func readFile(controllerVersion version.Number, source interface{}) (*file, error) { |
| 100 | readFunc, err := getFileDeserializationFunc(controllerVersion) |
| 101 | if err != nil { |
| 102 | return nil, errors.Trace(err) |
| 103 | } |
| 104 | |
| 105 | checker := schema.StringMap(schema.Any()) |
| 106 | coerced, err := checker.Coerce(source, nil) |
| 107 | if err != nil { |
| 108 | return nil, WrapWithDeserializationError(err, "file base schema check failed") |
| 109 | } |
| 110 | valid := coerced.(map[string]interface{}) |
| 111 | return readFunc(valid) |
| 112 | } |
| 113 | |
| 114 | func getFileDeserializationFunc(controllerVersion version.Number) (fileDeserializationFunc, error) { |
| 115 | var deserialisationVersion version.Number |
| 116 | for v := range fileDeserializationFuncs { |
| 117 | if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 { |
| 118 | deserialisationVersion = v |
| 119 | } |
| 120 | } |
| 121 | if deserialisationVersion == version.Zero { |
| 122 | return nil, NewUnsupportedVersionError("no file read func for version %s", controllerVersion) |
| 123 | } |
| 124 | return fileDeserializationFuncs[deserialisationVersion], nil |
| 125 | } |
| 126 | |
| 127 | // readFileList expects the values of the sourceList to be string maps. |
| 128 | func readFileList(sourceList []interface{}, readFunc fileDeserializationFunc) ([]*file, error) { |
| 129 | result := make([]*file, 0, len(sourceList)) |
| 130 | for i, value := range sourceList { |
| 131 | source, ok := value.(map[string]interface{}) |
| 132 | if !ok { |
| 133 | return nil, NewDeserializationError("unexpected value for file %d, %T", i, value) |
| 134 | } |
| 135 | file, err := readFunc(source) |
| 136 | if err != nil { |
| 137 | return nil, errors.Annotatef(err, "file %d", i) |
| 138 | } |
| 139 | result = append(result, file) |
| 140 | } |
| 141 | return result, nil |
| 142 | } |
| 143 | |
| 144 | type fileDeserializationFunc func(map[string]interface{}) (*file, error) |
| 145 | |
| 146 | var fileDeserializationFuncs = map[version.Number]fileDeserializationFunc{ |
| 147 | twoDotOh: file_2_0, |
| 148 | } |
| 149 | |
| 150 | func file_2_0(source map[string]interface{}) (*file, error) { |
| 151 | fields := schema.Fields{ |
| 152 | "resource_uri": schema.String(), |
| 153 | "filename": schema.String(), |
| 154 | "anon_resource_uri": schema.String(), |
| 155 | "content": schema.String(), |
| 156 | } |
| 157 | defaults := schema.Defaults{ |
| 158 | "content": "", |
| 159 | } |
| 160 | checker := schema.FieldMap(fields, defaults) |
| 161 | coerced, err := checker.Coerce(source, nil) |
| 162 | if err != nil { |
| 163 | return nil, WrapWithDeserializationError(err, "file 2.0 schema check failed") |
| 164 | } |
| 165 | valid := coerced.(map[string]interface{}) |
| 166 | // From here we know that the map returned from the schema coercion |
| 167 | // contains fields of the right type. |
| 168 | |
| 169 | anonURI, err := url.ParseRequestURI(valid["anon_resource_uri"].(string)) |
| 170 | if err != nil { |
| 171 | return nil, NewUnexpectedError(err) |
| 172 | } |
| 173 | |
| 174 | result := &file{ |
| 175 | resourceURI: valid["resource_uri"].(string), |
| 176 | filename: valid["filename"].(string), |
| 177 | anonymousURI: anonURI, |
| 178 | content: valid["content"].(string), |
| 179 | } |
| 180 | return result, nil |
| 181 | } |