diff --git a/README.md b/README.md deleted file mode 100644 index e0b8e82..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# rename-ext - -changes file-extension based on mime-type \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..d331987 --- /dev/null +++ b/install.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# rename-ext install script + +PREFIX=~/.local + +git clone --depth 1 \ + 'https://source.garden/dym/rename-ext.git' \ + $PREFIX/src/rename-ext/ + +ln -s $PREFIX/src/rename-ext/rename-ext.sh \ + $PREFIX/bin/rename-ext diff --git a/meta.kdl b/meta.kdl new file mode 100644 index 0000000..60e8fc7 --- /dev/null +++ b/meta.kdl @@ -0,0 +1,7 @@ +title "rename-ext" +description "changes file-extension based on mime-type" +type "code" +tags "script" "bash" "file-management" +license "AGPL" +homepage "https://dym.sh/rename-ext" +source "https://source.garden/dym/rename-ext" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9d3f21c --- /dev/null +++ b/readme.md @@ -0,0 +1,51 @@ +# rename-ext + +> changes file-extension based on mime-type + +### also + +- removes `*?|^:"<>` characters from the file-name +- reducing multiple spaces, dots, and underscores +- adds rename-date suffix to ensure uniquness of the filename +- strips exec-flag from non-executive types (usual leftover from FAT/NTFS) +- optimizes images while at it (shows reduction size in `kb` and `%`) + + +## install + +1. look at [install](./install.sh) file for instructions +2. adjust `PREFIX` at wish +3. make sure the final directory is on `$PATH` + + +## use + +`rename-ext /Data/Pictures/_unsorted/**/*` + +to only change extension and nothing else: + +`rename-ext --only-ext /Data/Photos/2020/*` + +to only print a report: + +`rename-ext --test-run /Data/Docs/_dump/*` + + +## requires + +- [`sd`](https://github.com/chmln/sd) – better sed +- [`pngcrush`](https://github.com/Kjuly/pngcrush) – to compress PNGs + +- [`jpegoptim`](https://github.com/tjko/jpegoptim) – to compress JPEGs + + +## todo +- [ ] find better determination tool for `application/octet-stream` +- [ ] leverage `fd` to do recursive paralel execution if some of parameters are folders + + +## q&a + +**Q**: why not just rename files to hashes of their contents, and store original filenames in some database? + +**A**: good point. that database is in my case a filesystem itself. diff --git a/rename-ext.sh b/rename-ext.sh new file mode 100755 index 0000000..8d32014 --- /dev/null +++ b/rename-ext.sh @@ -0,0 +1,311 @@ +#!/bin/bash + +## rename-ext +# > changes file-extension based on mime-type + +## also +# - removes `*?|^:"<>` characters from the file-name +# - reducing multiple spaces, dots, and underscores +# - adds rename-date suffix to ensure uniquness of the filename +# - strips exec-flag from non-executive types (usual leftover from FAT/NTFS) +# - optimizes images while at it (shows reduction size in `kb` and `%`) + +## example +# rename-ext /Data/Pictures/_unsorted/**/* + +## requires +# - `cargo install sd` – better sed +# - pngcrush – to compress PNGs (losless) +# // - pngquant – to compress PNGs (lossy) +# - jpegoptim – to compress JPEGs + + +ALL_EXTS=`echo \ + 'gif|jpe?g|jp2|jfif|a?png|web(p|m)|svg + |eps|tga|tiff?|psd|ico|xcf|heic + |eot|otf|ttf|epub|doc|docx|xls|swf|pdf|odt + |flac|opus|ogg|m4a|wav + |mpe?g|mp\d|mov|mkv|avif?|asf|3gpp? + |html?|sh|py|php + ' \ + | tr -d '[:space:]' \ + ` + +mkdir -p ~/tmp + + +OPTIONS='' +CHX_CHANGELOG='' +NR='1' + + +calc_reduction() +{ + SIZE_PRE="$1" + SIZE_POST="$2" + PERECENT=` echo "scale=2; 100 - (100 * $SIZE_POST / $SIZE_PRE)" | bc ` + RESULT="$(( $SIZE_PRE / 1000 ))-$(( ( $SIZE_PRE - $SIZE_POST ) / 1000 )) kb \ +($PERECENT%) +" + echo "$RESULT" +} + + +ch_x() +{ + FILENAME="$1" + # echo "FILENAME = '$FILENAME'" + + CHX_CHANGELOG='' + + IS_X_INTENDED='0' + if [ ! -z "$2" ]; then + IS_X_INTENDED='1' + fi + # echo "IS_X_INTENDED = '$IS_X_INTENDED'" + + IS_X=`stat -c '%A' "$FILENAME" | grep 'x' | wc -l` + # echo "IS_X = '$IS_X'" + + MODE='-x' + if [[ '1' == "$IS_X_INTENDED" ]]; then + MODE='+x' + fi + # echo "MODE = '$MODE'" + + if [[ ! "$OPTIONS" =~ 'test-run' ]]; then + # echo '(not a test-run)' + if [[ ! "$OPTIONS" =~ 'only-ext' ]]; then + # echo '(not only-ext)' + if [[ ! "$IS_X" == "$IS_X_INTENDED" ]]; then + CHX_CHANGELOG="chmod $MODE" + fi + chmod "$MODE" "$FILENAME" + fi + fi +} + + +rename_ext() +{ + NAME="$1" + EXT="$2" + NOTE='' + TARGET='' + + ch_x "$FILENAME" + + if [[ "$OPTIONS" =~ 'only-ext' ]]; then + NAME_NO_EXT=` echo "$NAME" \ + | sd -f i "(\.+($ALL_EXTS))$" '' \ + ` + TARGET="$NAME_NO_EXT.$EXT" + else + if [[ ! "$OPTIONS" =~ 'test-run' ]]; then + + SIZE_PRE=` stat "$FILENAME" -c '%s' ` + + [ 'jpg' = "$EXT" ] \ + && jpegoptim "$FILENAME" -qsftp + if [ 'png' = "$EXT" ]; then + mogrify "$FILENAME" + pngcrush -nolimits -s "$FILENAME" "$FILENAME.tmp.png" + # pngquant "$FILENAME" --output "$FILENAME" \ + # --quality=100 --force --skip-if-larger --speed 1 + if [ -f "$FILENAME.tmp.png" ] \ + && [ -s "$FILENAME.tmp.png" ] + then + mv "$FILENAME.tmp.png" "$FILENAME" + else + rm "$FILENAME.tmp.png" + fi + fi + + SIZE_POST=` stat "$FILENAME" -c '%s' ` + + [ $SIZE_PRE -gt $SIZE_POST ] \ + && NOTE=` calc_reduction "$SIZE_PRE" "$SIZE_POST" ` + fi + DATE=` date -r "$NAME" -u "+%Y%m%d%H%M%S" ` + TCLEAN=` echo "$NAME" \ + | sd '[\\\?\*\|^:"<>]+' '_' \ + | sd '_\d{12,}\.' '.' \ + | sd -f i "(\.+($ALL_EXTS))+$" '' \ + ` + TARGET=` echo "${TCLEAN}_$DATE.$EXT" \ + | sd '([\s\.,_]){3,}' '$1' \ + ` + fi + + IS_SAME_NAME='0' + if [[ "$NAME" == "$TARGET" ]]; then + IS_SAME_NAME='1' + fi + # echo "IS_SAME_NAME = '$IS_SAME_NAME'" + + # echo "CHX_CHANGELOG = '$CHX_CHANGELOG'" + if [[ '1' == "$IS_SAME_NAME" ]] \ + && [ -z "$CHX_CHANGELOG" ] + then + return + fi + + echo "$NR:" + NR="$(($NR+1))" + echo " $FILENAME" + + if [ ! -z "$CHX_CHANGELOG" ]; then + echo " $CHX_CHANGELOG" + CHX_CHANGELOG='' + fi + + if [[ '1' == "$IS_SAME_NAME" ]]; then + return + fi + + + if [[ "$OPTIONS" =~ 'test-run' ]]; then + if [ -f "$TARGET" ]; then + if [[ "$PARAMS" =~ 'force-replace' ]]; then + echo " >> replacing existing file >>" + else + echo " !! file already exist !!" + fi + fi + echo "$TARGET" + else + if [ ! -f "$TARGET" ]; then + echo " >> $NOTE >>" + echo " $TARGET" + mv "$NAME" "$TARGET" + else + if [[ "$PARAMS" =~ 'force-replace' ]]; then + echo " >> $NOTE ## replacing existing file >>" + echo " $TARGET" + mv "$NAME" "$TARGET" + else + echo ' !! File already exist, use `--force-replace` to overwrite !! ' + echo " $TARGET" + fi + fi + fi + +} + +for PARAM in "$@"; do + # echo "PARAM = '$PARAM'" + if [[ '--only-ext' == "$PARAM" ]]; then + OPTIONS="$OPTIONS;only-ext" + # echo "OPTIONS = '$OPTIONS'" + elif [[ '--test-run' == "$PARAM" ]]; then + OPTIONS="$OPTIONS;test-run" + # echo "OPTIONS = '$OPTIONS'" + elif [[ '--force-replace' == "$PARAM" ]]; then + OPTIONS="$OPTIONS;force-replace" + # echo "OPTIONS = '$OPTIONS'" + fi +done + +[ ! -z "$OPTIONS" ] \ + && echo "OPTIONS: '$OPTIONS'" + + +# echo "@ = '$@'" +for FILENAME in "$@"; do + + # echo "FILENAME = '$FILENAME'" + + # NOT_F_=`[ ! -f "$FILENAME" ]` + # echo "NOT_F_ = '$NOT_F_'" + + # [ ! -f "$FILENAME" ] \ + # && continue + + FILE_TYPE=` file -b --mime-type "$FILENAME" ` + # echo "FILE_TYPE = '$FILE_TYPE'" + + case "$FILE_TYPE" in + + # pixel-based + 'image/gif') rename_ext "$FILENAME" 'gif' ;; + 'image/jpeg') rename_ext "$FILENAME" 'jpg' ;; + 'image/jp2') rename_ext "$FILENAME" 'jp2' ;; + 'image/png') rename_ext "$FILENAME" 'png' ;; + 'image/webp') rename_ext "$FILENAME" 'webp' ;; + + # vector-based + 'image/svg+xml') rename_ext "$FILENAME" 'svg' ;; + 'image/x-eps') rename_ext "$FILENAME" 'eps' ;; + + # some less common image formats + 'image/heic') rename_ext "$FILENAME" 'heic' ;; + 'image/tiff') rename_ext "$FILENAME" 'tiff' ;; + 'image/vnd.adobe.photoshop') rename_ext "$FILENAME" 'psd' ;; + 'image/vnd.microsoft.icon') rename_ext "$FILENAME" 'ico' ;; + 'image/x-tga') rename_ext "$FILENAME" 'tga' ;; + 'image/x-xcf') rename_ext "$FILENAME" 'xcf' ;; + + # fonts + 'application/vnd.ms-fontobject') rename_ext "$FILENAME" 'eot' ;; + 'application/vnd.ms-opentype') rename_ext "$FILENAME" 'otf' ;; + 'font/sfnt') rename_ext "$FILENAME" 'ttf' ;; + + # documents + 'application/epub+zip') rename_ext "$FILENAME" 'epub' ;; + 'application/msword') rename_ext "$FILENAME" 'doc' ;; + 'application/pdf') ch_x "$FILENAME" ;; # can be .ai + 'application/vnd.ms-excel') rename_ext "$FILENAME" 'xls' ;; + 'application/vnd.oasis.opendocument.text') rename_ext "$FILENAME" 'odt' ;; + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') rename_ext "$FILENAME" 'docx' ;; + 'application/x-shockwave-flash') rename_ext "$FILENAME" 'swf' ;; + + # audio + 'audio/flac') rename_ext "$FILENAME" 'flac' ;; + 'audio/mpeg') rename_ext "$FILENAME" 'mp3' ;; + 'audio/ogg') rename_ext "$FILENAME" 'ogg' ;; + 'audio/x-m4a') rename_ext "$FILENAME" 'm4a' ;; + 'audio/x-wav') rename_ext "$FILENAME" 'wav' ;; + + # video + 'video/3gpp') rename_ext "$FILENAME" '3gp' ;; + 'video/av1') rename_ext "$FILENAME" 'avi' ;; + 'video/MP2T') rename_ext "$FILENAME" 'mp2' ;; + 'video/mp4') rename_ext "$FILENAME" 'mp4' ;; + 'video/quicktime') rename_ext "$FILENAME" 'mov' ;; + 'video/webm') rename_ext "$FILENAME" 'webm' ;; + 'video/x-m4v') rename_ext "$FILENAME" 'mp4' ;; + 'video/x-matroska') rename_ext "$FILENAME" 'mkv' ;; + 'video/x-ms-asf') rename_ext "$FILENAME" 'asf' ;; + + # text + 'application/json') ch_x "$FILENAME" ;; # can be any other language + 'text/html') ch_x "$FILENAME" ;; # can be .htm, .htc, .mht, ... + 'text/xml') ch_x "$FILENAME" ;; # can be .xml, .opml, .rss, ... + 'text/plain') ;; # can be any file with not enough text + 'text/x-python') ;; # can be .py, .py3, have no .ext + 'text/x-shellscript') ;; # can be .sh, .zsh, have no .ext + + # binady data + 'application/x-sqlite3') ch_x "$FILENAME" ;; # can be .db, .sql, .sq3 + + # special + 'application/octet-stream') ;; # can be anything + 'inode/directory') + # echo "$FILENAME is a folder" + # echo "about to ch_x" + ch_x "$FILENAME" '+' + ;; # a folder + 'inode/x-empty') ch_x "$FILENAME" ;; # zero-size + + # default + *) echo "?? $FILENAME : $FILE_TYPE" ;; + + esac + +done + +# if [[ "@#" -eq 1 ]]; then +# if [[ `file -b --mime-type "$1"` == "inode/directory" ]]; then +# find "$1" +# fi +# fi