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  }