github.com/rigado/snapd@v2.42.5-go-mod+incompatible/client/snap_op.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package client 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "io" 27 "mime/multipart" 28 "os" 29 "path/filepath" 30 ) 31 32 type SnapOptions struct { 33 Channel string `json:"channel,omitempty"` 34 Revision string `json:"revision,omitempty"` 35 CohortKey string `json:"cohort-key,omitempty"` 36 LeaveCohort bool `json:"leave-cohort,omitempty"` 37 DevMode bool `json:"devmode,omitempty"` 38 JailMode bool `json:"jailmode,omitempty"` 39 Classic bool `json:"classic,omitempty"` 40 Dangerous bool `json:"dangerous,omitempty"` 41 IgnoreValidation bool `json:"ignore-validation,omitempty"` 42 Unaliased bool `json:"unaliased,omitempty"` 43 Purge bool `json:"purge,omitempty"` 44 Amend bool `json:"amend,omitempty"` 45 46 Users []string `json:"users,omitempty"` 47 } 48 49 func writeFieldBool(mw *multipart.Writer, key string, val bool) error { 50 if !val { 51 return nil 52 } 53 return mw.WriteField(key, "true") 54 } 55 56 func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error { 57 fields := []struct { 58 f string 59 b bool 60 }{ 61 {"devmode", opts.DevMode}, 62 {"classic", opts.Classic}, 63 {"jailmode", opts.JailMode}, 64 {"dangerous", opts.Dangerous}, 65 } 66 for _, o := range fields { 67 if err := writeFieldBool(mw, o.f, o.b); err != nil { 68 return err 69 } 70 } 71 72 return nil 73 } 74 75 func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error { 76 return writeFieldBool(mw, "unaliased", opts.Unaliased) 77 } 78 79 type actionData struct { 80 Action string `json:"action"` 81 Name string `json:"name,omitempty"` 82 SnapPath string `json:"snap-path,omitempty"` 83 *SnapOptions 84 } 85 86 type multiActionData struct { 87 Action string `json:"action"` 88 Snaps []string `json:"snaps,omitempty"` 89 Users []string `json:"users,omitempty"` 90 } 91 92 // Install adds the snap with the given name from the given channel (or 93 // the system default channel if not). 94 func (client *Client) Install(name string, options *SnapOptions) (changeID string, err error) { 95 return client.doSnapAction("install", name, options) 96 } 97 98 func (client *Client) InstallMany(names []string, options *SnapOptions) (changeID string, err error) { 99 return client.doMultiSnapAction("install", names, options) 100 } 101 102 // Remove removes the snap with the given name. 103 func (client *Client) Remove(name string, options *SnapOptions) (changeID string, err error) { 104 return client.doSnapAction("remove", name, options) 105 } 106 107 func (client *Client) RemoveMany(names []string, options *SnapOptions) (changeID string, err error) { 108 return client.doMultiSnapAction("remove", names, options) 109 } 110 111 // Refresh refreshes the snap with the given name (switching it to track 112 // the given channel if given). 113 func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) { 114 return client.doSnapAction("refresh", name, options) 115 } 116 117 func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) { 118 return client.doMultiSnapAction("refresh", names, options) 119 } 120 121 func (client *Client) Enable(name string, options *SnapOptions) (changeID string, err error) { 122 return client.doSnapAction("enable", name, options) 123 } 124 125 func (client *Client) Disable(name string, options *SnapOptions) (changeID string, err error) { 126 return client.doSnapAction("disable", name, options) 127 } 128 129 // Revert rolls the snap back to the previous on-disk state 130 func (client *Client) Revert(name string, options *SnapOptions) (changeID string, err error) { 131 return client.doSnapAction("revert", name, options) 132 } 133 134 // Switch moves the snap to a different channel without a refresh 135 func (client *Client) Switch(name string, options *SnapOptions) (changeID string, err error) { 136 return client.doSnapAction("switch", name, options) 137 } 138 139 // SnapshotMany snapshots many snaps (all, if names empty) for many users (all, if users is empty). 140 func (client *Client) SnapshotMany(names []string, users []string) (setID uint64, changeID string, err error) { 141 result, changeID, err := client.doMultiSnapActionFull("snapshot", names, &SnapOptions{Users: users}) 142 if err != nil { 143 return 0, "", err 144 } 145 if len(result) == 0 { 146 return 0, "", fmt.Errorf("server result does not contain snapshot set identifier") 147 } 148 var x struct { 149 SetID uint64 `json:"set-id"` 150 } 151 if err := json.Unmarshal(result, &x); err != nil { 152 return 0, "", err 153 } 154 return x.SetID, changeID, nil 155 } 156 157 var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file") 158 159 func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) { 160 if options != nil && options.Dangerous { 161 return "", ErrDangerousNotApplicable 162 } 163 action := actionData{ 164 Action: actionName, 165 SnapOptions: options, 166 } 167 data, err := json.Marshal(&action) 168 if err != nil { 169 return "", fmt.Errorf("cannot marshal snap action: %s", err) 170 } 171 path := fmt.Sprintf("/v2/snaps/%s", snapName) 172 173 headers := map[string]string{ 174 "Content-Type": "application/json", 175 } 176 177 return client.doAsync("POST", path, nil, headers, bytes.NewBuffer(data)) 178 } 179 180 func (client *Client) doMultiSnapAction(actionName string, snaps []string, options *SnapOptions) (changeID string, err error) { 181 if options != nil { 182 return "", fmt.Errorf("cannot use options for multi-action") // (yet) 183 } 184 _, changeID, err = client.doMultiSnapActionFull(actionName, snaps, options) 185 186 return changeID, err 187 } 188 189 func (client *Client) doMultiSnapActionFull(actionName string, snaps []string, options *SnapOptions) (result json.RawMessage, changeID string, err error) { 190 action := multiActionData{ 191 Action: actionName, 192 Snaps: snaps, 193 } 194 if options != nil { 195 action.Users = options.Users 196 } 197 data, err := json.Marshal(&action) 198 if err != nil { 199 return nil, "", fmt.Errorf("cannot marshal multi-snap action: %s", err) 200 } 201 202 headers := map[string]string{ 203 "Content-Type": "application/json", 204 } 205 206 return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data)) 207 } 208 209 // InstallPath sideloads the snap with the given path under optional provided name, 210 // returning the UUID of the background operation upon success. 211 func (client *Client) InstallPath(path, name string, options *SnapOptions) (changeID string, err error) { 212 f, err := os.Open(path) 213 if err != nil { 214 return "", fmt.Errorf("cannot open: %q", path) 215 } 216 217 action := actionData{ 218 Action: "install", 219 Name: name, 220 SnapPath: path, 221 SnapOptions: options, 222 } 223 224 pr, pw := io.Pipe() 225 mw := multipart.NewWriter(pw) 226 go sendSnapFile(path, f, pw, mw, &action) 227 228 headers := map[string]string{ 229 "Content-Type": mw.FormDataContentType(), 230 } 231 232 return client.doAsync("POST", "/v2/snaps", nil, headers, pr) 233 } 234 235 // Try 236 func (client *Client) Try(path string, options *SnapOptions) (changeID string, err error) { 237 if options == nil { 238 options = &SnapOptions{} 239 } 240 if options.Dangerous { 241 return "", ErrDangerousNotApplicable 242 } 243 244 buf := bytes.NewBuffer(nil) 245 mw := multipart.NewWriter(buf) 246 mw.WriteField("action", "try") 247 mw.WriteField("snap-path", path) 248 options.writeModeFields(mw) 249 mw.Close() 250 251 headers := map[string]string{ 252 "Content-Type": mw.FormDataContentType(), 253 } 254 255 return client.doAsync("POST", "/v2/snaps", nil, headers, buf) 256 } 257 258 func sendSnapFile(snapPath string, snapFile *os.File, pw *io.PipeWriter, mw *multipart.Writer, action *actionData) { 259 defer snapFile.Close() 260 261 if action.SnapOptions == nil { 262 action.SnapOptions = &SnapOptions{} 263 } 264 fields := []struct { 265 name string 266 value string 267 }{ 268 {"action", action.Action}, 269 {"name", action.Name}, 270 {"snap-path", action.SnapPath}, 271 {"channel", action.Channel}, 272 } 273 for _, s := range fields { 274 if s.value == "" { 275 continue 276 } 277 if err := mw.WriteField(s.name, s.value); err != nil { 278 pw.CloseWithError(err) 279 return 280 } 281 } 282 283 if err := action.writeModeFields(mw); err != nil { 284 pw.CloseWithError(err) 285 return 286 } 287 288 if err := action.writeOptionFields(mw); err != nil { 289 pw.CloseWithError(err) 290 return 291 } 292 293 fw, err := mw.CreateFormFile("snap", filepath.Base(snapPath)) 294 if err != nil { 295 pw.CloseWithError(err) 296 return 297 } 298 299 _, err = io.Copy(fw, snapFile) 300 if err != nil { 301 pw.CloseWithError(err) 302 return 303 } 304 305 mw.Close() 306 pw.Close() 307 }