#!/usr/bin/env bash # # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)store-object 3.3.1 16.03.2026 (c)2026 Standby.cloud # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ # # This script can be used free of charge. Use it as is or customize as needed. It is not guaranteed to be # error free and you will not be reimbursed for any damage it may cause. # #@ Export an OS image, a local file or all files from given local folder to ObjectStorage. #@ #@Usage: store-object [options] [object] #@ Options: -h, --help : Displays helptext. #@ -v, --version: Displays the version of the script. #@ -q, --quiet : Don't display any error messages. #@ -f, --force : Force overwriting. #@ -p, --prefix : Prefix used for ObjectStorage. #@ -u, --url : Specify an URL to write to ObjectStorage. #@ -i, --image : Store an OS image in ObjectStorage. #@ Object: : Can be a file, folder or image e.g. "Bastion.oci" or "Base.oci". #@ If not specified, list capabilities of URL. #@ #@Examples: #@ store-object ~/.ssh #@ Zips all the content under ~/.ssh and exports it to Object Storage # # Exit codes: # 01: Unknown parameter or wrong URL. # 02: Not a valid image. # 03: Not valid file or folder. # 04: No curl or zip in PATH. # 05: Error while zipping. # 06: Unable to upload file to ObjectStorage. # 07: RCLONE not usable. # 99: User interrupt. # # Update history: # # V 1.0.0 22.03.2021 First version # V 3.0.0 15.11.2021 Export any directory # V 3.1.0 05.06.2023 New copyright # V 3.2.0 13.09.2024 New minor version # V 3.2.1 21.08.2025 Import also OS images and script renamed to store-object # V 3.3.0 19.01.2026 Revised with support of Claude Code # V 3.3.1 16.03.2026 Optimized: replaced backticks with $(); [ ] with [[ ]]; # echo|grep with [[ == pattern ]]; replaced [ -a ] with &&; # replaced [ -o ] with ||; UUOC removed # # Find executable bash library and source it lib=$(command -v lib.bash 2>/dev/null) if [[ -n "$lib" ]]; then source "$lib" else progdir=$(dirname "$0") if [[ -r "${progdir}/lib.bash" ]]; then source "${progdir}/lib.bash" else echo "Unexpected error: Unable to locate bash library 'lib.bash'." exit 1 fi fi # Needed to download OCI OS images readonly imageurl="https://objectstorage.eu-frankfurt-1.oraclecloud.com/p/dAxQHw_fY2bz0agilOPilkLYji2Z2jMrxqrgxg01HcNGKPo_xzkh7exokWXjJV4q/n/frelkczkpnqg/b/Images/o" # Project information in JSON format readonly projectjson="${REALHOME}/.oci/project.json" # Do extra cleanup function ExtraCleanup() { filecheck -rm "${scratchfile}.zip" filecheck -rm "${scratchfile}.obj" } # Check if URL is a valid OCI ObjectStorage pre-auth URL ending in /o function CheckURL() { local url="${1%/}" # strip trailing slash with bash built-in local newurl="" local stat=1 if [[ -n "$url" ]]; then # Use bash pattern match instead of echo|grep subshell if [[ "$url" == https://objectstorage.*.oraclecloud.com/* ]]; then newurl=$(basename "$url") if [[ "$newurl" == "o" ]]; then newurl=$(dirname "$url") if [[ -n "$newurl" ]]; then printf '%s/o\n' "$newurl" stat=0 fi fi fi fi return $stat } # Return endpoint (hostname) from URL function GetEndpointFromURL() { local url="${1}" if [[ -n "$url" ]]; then # Strip https:// and take first path component local tmp="${url#https://}" printf '%s\n' "${tmp%%/*}" fi } # Return location (path) from URL function GetLocationFromURL() { local url="${1}" if [[ -n "$url" ]]; then local tmp="${url#https://}" printf '%s\n' "${tmp#*/}" fi } # Return region name from endpoint (second dot-separated field) function GetRegionFromEndpoint() { local ep="${1}" if [[ -n "$ep" ]]; then local retstr="${ep#*.}" # strip first label retstr="${retstr%%.*}" # keep only second label retstr="${retstr,,}" # lowercase with bash built-in # Validate region pattern x-name-n if [[ "$retstr" =~ ^..-.*-.$ ]]; then printf '%s\n' "$retstr" fi fi } # Return pre-auth token from location (second path segment if location starts with p/) function GetPreauthFromLocation() { local loc="${1}" if [[ -n "$loc" ]]; then if [[ "$loc" == p/* ]]; then local tmp="${loc#p/}" printf '%s\n' "${tmp%%/*}" fi fi } # Return namespace from location function GetNamespaceFromLocation() { local loc="${1}" if [[ -n "$loc" ]]; then local rest="$loc" # Skip pre-auth segment if present [[ "$rest" == p/* ]] && rest="${rest#p/*/}" if [[ "$rest" == n/* ]]; then rest="${rest#n/}" printf '%s\n' "${rest%%/*}" fi fi } # Return bucket name from location function GetBucketFromLocation() { local loc="${1}" if [[ -n "$loc" ]]; then local d1 d1=$(dirname "$loc") if [[ -n "$d1" ]]; then local d2 d2=$(dirname "$d1") if [[ -n "$d2" ]]; then local bparent bparent=$(basename "$d2") if [[ "$bparent" == "b" ]]; then basename "$d1" fi fi fi fi } # Check if we got an error from objectstorage function CheckError() { if [[ -r "$scratchfile" ]]; then local grepres grepres=$(grep '^{"code":"' "$scratchfile") if [[ -n "$grepres" ]]; then printf '%s\n' "$grepres" | cut -d'"' -f4 fi fi } # Read all objects from url function GetObjects() { local url="${1}" local code="" local stat=1 if [[ -n "$url" && -n "$curl" ]]; then $curl -s "${url}/" -o "$scratchfile" stat=$? if [[ $stat -eq 0 ]]; then code=$(CheckError) if [[ -n "$code" ]]; then stat=2 else if [[ -z "$jq" ]]; then stat=3 else # Avoid UUOC: redirect directly "$jq" -r ".objects[].name" < "$scratchfile" > "${scratchfile}.obj" stat=$? [[ $stat -ne 0 ]] && rm -f "${scratchfile}.obj" fi fi fi fi return $stat } # Check if the given object exists in storage function CheckObjects() { local url="${1}" local object="${2}" local name="" GetObjects "$url" local stat=$? if [[ $stat -eq 0 ]]; then name=$(grep "^${object}$" "${scratchfile}.obj") printf '%s\n' "$name" fi filecheck -rm "$scratchfile" filecheck -rm "${scratchfile}.obj" } function CheckToOverwrite() { local object="${1}" local name="" local stat=0 if [[ -n "$readuri" && -n "$object" ]]; then name=$(CheckObjects "$readuri" "$object") if [[ "$name" == "$object" ]]; then confirm "Object '$object' is already uploaded. Overwrite it?" stat=$? fi fi return $stat } # Preset param="" prefix="" specuri="" writeuri="" readuri="" bucketname="" regionname="" endpoint="" location="" preauth="" filename="" quietstr="" force=false storeimage=false havefolder=false # Check parameters: Loop until all parameters are used up while [[ $# -gt 0 ]]; do pname="${1}" case "$pname" in -v | --version) shift showversion=true ;; -h | --help) shift showhelp=true ;; -f | --force) shift force=true ;; -q | --quiet) shift quietstr="$pname" ;; -i | --image) shift storeimage=true ;; -p | --prefix) shift if [[ -n "$1" ]]; then if [[ -z "$prefix" ]]; then # Strip leading/trailing slashes and squeeze duplicates prefix=$(printf '%s' "$1" | tr -s '/' | sed 's|^/||' | sed 's|/$||') else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a prefix after parameter '$pname'." fi ;; -u | --url) shift if [[ -n "$1" ]]; then if [[ -z "$specuri" ]]; then specuri="$1" else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify an URL after parameter '$pname'." fi ;; *) shift if [[ "$pname" == -* ]]; then errstr="Unknown option '$pname'." else if [[ -z "$param" ]]; then param="$pname" else errstr="Object was already specified '$param'. Unknown additional parameter: '$pname'." fi fi ;; esac done # Check if oci cli is configured and we have a project definition if URL was not specified if [[ -z "$specuri" && -r "$projectjson" ]]; then writeuri=$(browse-json project/writeuri --quiet --raw --select 1 --import "$projectjson") readuri=$(browse-json project/readuri --quiet --raw --select 1 --import "$projectjson") fi # Plausibility checks if [[ -z "$specuri" && -z "$writeuri" ]]; then errstr="No URL specified to write to ObjectStorage or defined in '$projectjson'." else if [[ -n "$specuri" ]]; then writeuri=$(CheckURL "$specuri") else writeuri=$(CheckURL "$writeuri") readuri=$(CheckURL "$readuri") fi if [[ -n "$writeuri" ]]; then endpoint=$(GetEndpointFromURL "$writeuri") regionname=$(GetRegionFromEndpoint "$endpoint") location=$(GetLocationFromURL "$writeuri") preauth=$(GetPreauthFromLocation "$location") namespace=$(GetNamespaceFromLocation "$location") bucketname=$(GetBucketFromLocation "$location") fi if [[ -z "$bucketname" || -z "$regionname" ]]; then errstr="Invalid URL specified to write to ObjectStorage." fi fi # Display help or error message DisplayHelp # Main # Check if we have needed tools installed rclone=$(filecheck -x rclone) curl=$(filecheck -x curl) zip=$(filecheck -x zip) jq=$(filecheck -x jq) # Check if rclone is configured for this region if [[ -n "$rclone" ]]; then rcloneregion=$($rclone -q listremotes | grep "^${regionname}:$") else rcloneregion="" fi if [[ -z "$param" ]]; then # No parameter specified - list capabilities of URL commentvers=$(grep '^# Version: @(' "$script" | head -n 1 | cut -d')' -f2-) namestr="${commentvers%% *}" cversion=$(printf '%s' "$commentvers" | cut -d' ' -f2) cright=$(printf '%s' "$commentvers" | cut -d' ' -f4) cowner=$(printf '%s' "$commentvers" | cut -d' ' -f5-) printf "\n%s %s, %s %s\n\n" "$namestr" "$cversion" "$cright" "$cowner" if [[ "$storeimage" == true ]]; then GetObjects "$imageurl" stat=$? if [[ $stat -eq 0 && -r "${scratchfile}.obj" ]]; then printf 'Images found:\n' cat "${scratchfile}.obj" rm -f "${scratchfile}.obj" fi else [[ -n "$readuri" ]] && printf " readuri: %s\n" "$readuri" printf " writeuri: %s\n" "$writeuri" printf " endpoint: %s\n" "$endpoint" printf " regionname: %s\n" "$regionname" printf " namespace: %s\n" "$namespace" printf " bucketname: %s\n" "$bucketname" if [[ -n "$preauth" ]]; then printf " usingpreauth: true\n" else printf " usingpreauth: false\n" fi GetObjects "$writeuri" stat=$? if [[ $stat -eq 0 ]]; then printf "objectlisting: true\n" else printf "objectlisting: false\n" fi if [[ -n "$rcloneregion" ]]; then imagename=$(basename "$scratchfile") $rclone -q copyto "$scratchfile" "${rcloneregion}/${bucketname}/${imagename}" stat=$? if [[ $stat -eq 0 ]]; then printf "rcloneenabled: true\n" $curl -s "${writeuri}/${imagename}" -o "$scratchfile" stat=$? if [[ $stat -eq 0 ]]; then code=$(CheckError) [[ -n "$code" ]] && printf " readable: false\n" || printf " readable: true\n" else printf " readable: false\n" fi $curl -sT "$scratchfile" "${writeuri}/${imagename}" > "$scratchfile" 2>&1 stat=$? if [[ $stat -eq 0 ]]; then code=$(CheckError) [[ -n "$code" ]] && printf " writable: false\n" || printf " writable: true\n" else printf " writable: false\n" fi $rclone -q deletefile "${rcloneregion}/${bucketname}/${imagename}" fi else printf "rcloneenabled: false\n" printf " readable: unknown\n" printf " writable: unknown\n" fi fi else # Define filename if [[ "$storeimage" == true ]]; then filename=$(CheckObjects "$imageurl" "$param") if [[ "$param" != "$filename" ]]; then exitcode=2 errormsg $quietstr $exitcode "Specified object '$param' is not a valid image." else if [[ -z "$prefix" ]]; then filename="$param" else filename="${prefix}/${param}" fi fi else if [[ -d "$param" ]]; then havefolder=true parentdir=$(dirname "$param") basedir=$(basename "$param") if [[ -z "$prefix" ]]; then filename="${basedir}.zip" else filename="${prefix}/${basedir}.zip" fi elif [[ -f "$param" ]]; then basedir=$(basename "$param") if [[ -z "$prefix" ]]; then filename="$basedir" else filename="${prefix}/${basedir}" fi else exitcode=3 errormsg $quietstr $exitcode "Specified object '$param' is not valid file or folder." fi fi fi # Check if we want to copy an OS image or if we have a valid file or folder if [[ -n "$filename" && $exitcode -eq 0 ]]; then if [[ "$storeimage" == true ]]; then if [[ -z "$rclone" || -z "$rcloneregion" ]]; then exitcode=7 if [[ -z "$rcloneregion" ]]; then errormsg $quietstr $exitcode "Remote '${regionname}:' is not defined in 'rclone'." else errormsg $quietstr $exitcode "Tool 'rclone' not found in PATH '$PATH'" fi else if [[ "$force" == true ]]; then stat=0 else CheckToOverwrite "$filename" stat=$? fi if [[ $stat -eq 0 ]]; then $rclone -q copyurl "${imageurl}/${param}" "${rcloneregion}/${bucketname}/${filename}" stat=$? if [[ $stat -eq 0 ]]; then [[ -z "$quietstr" ]] && printf "Image '%s' was uploaded to ObjectStorage region '%s' and bucket '%s'.\n" \ "$param" "$regionname" "$bucketname" else exitcode=6 errormsg $quietstr $exitcode "Unable to upload file to ObjectStorage" fi fi fi else if [[ -n "$curl" && -n "$zip" ]]; then if [[ "$force" == true ]]; then stat=0 else CheckToOverwrite "$filename" stat=$? fi if [[ $stat -eq 0 ]]; then if [[ "$havefolder" == true ]]; then cd "$parentdir" $zip -r -q -y "${scratchfile}.zip" "$basedir" -x \*.DS_Store stat=$? if [[ $stat -eq 0 ]]; then $curl -sT "${scratchfile}.zip" "${writeuri}/${filename}" > "$scratchfile" 2>&1 stat=$? if [[ $stat -eq 0 ]]; then code=$(CheckError) if [[ -n "$code" ]]; then exitcode=6 errormsg $quietstr $exitcode "Unable to upload folder to ObjectStorage" else if [[ -z "$quietstr" ]]; then if [[ -z "$readuri" ]]; then printf "Folder '%s.zip' uploaded.\n" "$basedir" else printf "Folder zipped and uploaded. To download zip, please use this command:\n" printf "curl -s %s/%s -o %s.zip\n" "$readuri" "$filename" "$basedir" fi fi fi else exitcode=6 errormsg $quietstr $exitcode "Unable to upload folder to ObjectStorage" fi else exitcode=5 errormsg $quietstr $exitcode "Unable to zip '${HOME}/.ssh'" fi filecheck -rm "${scratchfile}.zip" else # Upload file $curl -sT "$param" "${writeuri}/${filename}" > "$scratchfile" 2>&1 stat=$? if [[ $stat -eq 0 ]]; then code=$(CheckError) if [[ -n "$code" ]]; then exitcode=6 errormsg $quietstr $exitcode "Unable to upload file to ObjectStorage" else if [[ -z "$quietstr" ]]; then if [[ -z "$readuri" ]]; then printf "File '%s' uploaded.\n" "$filename" else printf "File uploaded. To download file, please use this command:\n" printf "curl -s %s/%s -o %s\n" "$readuri" "$filename" "$param" fi fi fi else exitcode=6 errormsg $quietstr $exitcode "Unable to upload file to ObjectStorage" fi fi fi else exitcode=4 errormsg $quietstr $exitcode "Tool 'curl' or 'zip' not found in PATH '$PATH'" fi fi fi # Cleanup and exit Cleanup exit $exitcode