github.com/arduino/arduino-cloud-cli@v0.0.0-20240517070944-e7a449561083/command/ota/upload.go (about) 1 // This file is part of arduino-cloud-cli. 2 // 3 // Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published 7 // by the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 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 Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 package ota 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 "os" 25 "path/filepath" 26 27 "github.com/arduino/arduino-cli/cli/feedback" 28 "github.com/arduino/arduino-cloud-cli/config" 29 "github.com/arduino/arduino-cloud-cli/internal/iot" 30 "github.com/arduino/arduino-cloud-cli/internal/ota" 31 otaapi "github.com/arduino/arduino-cloud-cli/internal/ota-api" 32 ) 33 34 const ( 35 // default ota should complete in 10 mins 36 otaExpirationMins = 10 37 // deferred ota can take up to 1 week (equal to 10080 minutes) 38 otaDeferredExpirationMins = 10080 39 ) 40 41 // UploadParams contains the parameters needed to 42 // perform an OTA upload. 43 type UploadParams struct { 44 DeviceID string 45 File string 46 Deferred bool 47 DoNotApplyHeader bool 48 } 49 50 // Upload command is used to upload a firmware OTA, 51 // on a device of Arduino IoT Cloud. 52 func Upload(ctx context.Context, params *UploadParams, cred *config.Credentials) error { 53 _, err := os.Stat(params.File) 54 if err != nil { 55 return fmt.Errorf("file %s does not exists: %w", params.File, err) 56 } 57 58 iotClient, err := iot.NewClient(cred) 59 if err != nil { 60 return err 61 } 62 otapi := otaapi.NewClient(cred) 63 64 dev, err := iotClient.DeviceShow(ctx, params.DeviceID) 65 if err != nil { 66 return err 67 } 68 69 if !params.DoNotApplyHeader { 70 //Verify if file has already an OTA header 71 header, _ := ota.DecodeOtaFirmwareHeaderFromFile(params.File) 72 if header != nil { 73 params.DoNotApplyHeader = true 74 } 75 } 76 77 var otaFile string 78 if params.DoNotApplyHeader { 79 otaFile = params.File 80 } else { 81 otaDir, err := os.MkdirTemp("", "") 82 if err != nil { 83 return fmt.Errorf("%s: %w", "cannot create temporary folder", err) 84 } 85 otaFile = filepath.Join(otaDir, "temp.ota") 86 defer os.RemoveAll(otaDir) 87 88 err = Generate(params.File, otaFile, dev.Fqbn) 89 if err != nil { 90 return fmt.Errorf("%s: %w", "cannot generate .ota file", err) 91 } 92 } 93 94 file, err := os.Open(otaFile) 95 if err != nil { 96 return fmt.Errorf("%s: %w", "cannot open ota file", err) 97 } 98 defer file.Close() 99 100 expiration := otaExpirationMins 101 if params.Deferred { 102 expiration = otaDeferredExpirationMins 103 } 104 105 var conflictedOta *otaapi.Ota 106 err = iotClient.DeviceOTA(ctx, params.DeviceID, file, expiration) 107 if err != nil { 108 if errors.Is(err, iot.ErrOtaAlreadyInProgress) { 109 conflictedOta = &otaapi.Ota{ 110 DeviceID: params.DeviceID, 111 Status: "Skipped", 112 ErrorReason: "OTA already in progress", 113 } 114 } else { 115 return err 116 } 117 } 118 // Try to get ota-id from API 119 otaID, err := otapi.GetOtaLastStatusByDeviceID(params.DeviceID) 120 if err != nil { 121 return err 122 } 123 if otaID != nil && len(otaID.Ota) > 0 { 124 if conflictedOta != nil { 125 toPrint := otaapi.OtaStatusList{ 126 Ota: []otaapi.Ota{*conflictedOta, otaID.Ota[0]}, 127 } 128 feedback.PrintResult(toPrint) 129 } else { 130 feedback.PrintResult(otaID.Ota[0]) 131 } 132 } 133 134 return nil 135 }