diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..137d958 --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 jurakovic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c32aade --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ + +# Timestamp Copy + +Timestamps Copy is a lightweight Bash and PowerShell-based script that integrates directly into the Windows File Explorer context menu, enabling you to **copy** and **paste** file and folder timestamps with ease. + +This solution is especially useful when you need to preserve or replicate Date Created and Date Modified values across files or folders – ideal for organizing backups, restoring files, or syncing metadata. + +### Features + +#### Explorer Context Menu Integration + +Adds convenient right-click options for both files and folders: + +![ContextMenu](img/contextmenu.png) + +#### Copy Mode + +Stores the selected file or folder's Date Created and Date Modified timestamps for reuse. + +#### Paste Mode + +Applies the previously copied timestamps to the currently selected file or folder. + +#### Selective Timestamp Paste + +Use the specific `Paste 'Date Created'` or `Paste 'Date Modified'` options to update only the desired timestamp. + +### Usage + +Right-click on a file or folder and choose `Copy` under the context menu. +This saves the timestamps to a temporary location ("clipboard"). + +Right-click on another file or folder and choose: + +`Paste` – to apply both timestamps +`Paste 'Date Created'` – to apply only the Date Created +`Paste 'Date Modified'` – to apply only the Date Modified + +Each entry starts Bash terminal and runs the [`tscp.sh`](tscp.sh) script with the appropriate parameters (example screenshots below). + +### Requirements + +- Windows 10/11 (tested on Windows 11 24H2) +- PowerShell 5.1 or later +- Bash (recommended: Git for Windows) +- Administrator privileges (required for installation) + +> **Bash** can be installed on Windows in several ways, including: +> - [Git for Windows](https://gitforwindows.org) (comes with the MSYS2 runtime – [Git for Windows flavor](https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md)) +> - [MSYS2](https://www.msys2.org) +> - [Cygwin](https://cygwin.com) +> - [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install) +> +> The recommended way is to use **Git for Windows** as it provides a lightweight and user-friendly environment for running Bash scripts on Windows. +> This script is designed to work with Git for Windows and the official MSYS2 runtime. It doesn't work with Cygwin or WSL. It could work with some minor modifications, and while I don't plan to do it myself, feel free to update it to suit your own needs. + +### Installation + +1. Clone the repository. + ```bash + git clone https://github.com/jurakovic/timestamp-copy.git + ``` +2. Open an elevated Bash terminal ('Run as Administrator'). +3. Navigate to the directory where you cloned the repository. + ```bash + cd timestamp-copy + ``` +4. Add the context menu entries. This can be done in two ways. + Run the `tscp.sh` script + ```bash + ./tscp.sh + ``` + + and then choose the option `i` + ```text + Timestamp Copy (1.0.0) + + [i] Install + [u] Uninstall + + [q] Quit + + Choose option: + ``` + + or run the script with the `-i` option to install it directly: + ```bash + ./tscp.sh -i + ``` + +### Screenshots + +Copy +![Copy](img/copy.png) + +Paste +![Copy](img/paste.png) + +### Limitation + +This script is designed to work with **only one selected file or folder at a time**. While it does appear in the context menu when multiple items are selected, it will be executed **independently for each item**. This can lead to unexpected behavior. For accurate and predictable results, always use it with a single selection. + +### Disclaimer + +This script is provided **as-is**, without any warranties or guarantees of fitness for a particular purpose. It was created solely for educational and experimental use, and I do **not** intend to actively support or maintain it. While it should work reliably in most cases, use it at your own risk. + +### Future Plans + +In the future, I plan to develop a more robust and user-friendly version written entirely in PowerShell for a more native Windows experience. + +--- + +### References + + + + + + diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..6a6f62b --- /dev/null +++ b/_config.yml @@ -0,0 +1,3 @@ +remote_theme: jurakovic/cayman-blue +google_analytics: G-233EMZJM16 +dark_theme: true diff --git a/img/contextmenu.png b/img/contextmenu.png new file mode 100644 index 0000000..284e106 Binary files /dev/null and b/img/contextmenu.png differ diff --git a/img/copy.png b/img/copy.png new file mode 100644 index 0000000..93c4353 Binary files /dev/null and b/img/copy.png differ diff --git a/img/paste.png b/img/paste.png new file mode 100644 index 0000000..321b753 Binary files /dev/null and b/img/paste.png differ diff --git a/tscp.ico b/tscp.ico new file mode 100644 index 0000000..32d63ff Binary files /dev/null and b/tscp.ico differ diff --git a/tscp.sh b/tscp.sh new file mode 100644 index 0000000..919aeb2 --- /dev/null +++ b/tscp.sh @@ -0,0 +1,263 @@ +#!/bin/bash + +##### constants +homepage="https://github.com/jurakovic/timestamp-copy" +version="1.0.0" +bashPath="C:\Program Files\Git\usr\bin\bash.exe" +scriptPath="$(cygpath -w "$(pwd)")\tscp.sh" +iconPath="$(cygpath -w "$(pwd)")\tscp.ico" +fRootKey="HKEY_CLASSES_ROOT\*\shell\TimestampCopy" +dRootKey="HKEY_CLASSES_ROOT\Directory\shell\TimestampCopy" +clip_file="$HOME/.tscp" +datetime_format="yyyy-MM-dd HH:mm:ss" + +function main() { + if [ "$#" -eq 1 ] # cli arguments + then + case $1 in + -i|--install) install ;; + -u|--uninstall) uninstall ;; + -v|--version) echo "$version" ;; + -h|--help|-?) echo "For help visit $homepage" ;; + esac + elif [ "$#" -eq 2 ] # context menu commands + then + $1 "$2" + read -p "Press any key to exit..." -n1 -s; echo + else + show_menu + fi +} + +##### install/uninstall functions + +function show_menu() { + set +e + clear + echo + echo -e "Timestamp Copy ($version)" + echo " " + echo " [i] Install " + echo " [u] Uninstall " + echo " " + echo " [q] Quit " + echo + read -p "Choose option: " option + clear + __perform_action $option + if [ $option != "q" ] # else "quit" + then + read -p "Press any key to continue..." -n1 -s; echo + show_menu + fi +} + +function __perform_action() { + case $1 in + "i") install ;; + "u") uninstall ;; + "q") ;; # do nothing, will quit + *) echo "unknown option: $1" ;; + esac +} + +function install() { + net session 1>/dev/null 2>/dev/null + if [ ! $? -eq 0 ]; then + read -p "Not running as Admin. Press any key to exit..." -n1 -s; echo + exit 1 + fi + + echo "Installing..." + install_internal "$fRootKey" + install_internal "$dRootKey" + echo "Done" +} + +function install_internal() { + itemPath="$1\\shell" + add_menu_root "$1" "Timestamp Copy" "$iconPath" + add_menu_item "$itemPath\\010CopyDateCreatedModified" "Copy" "copy" + add_menu_item "$itemPath\\020PasteDateCreatedModified" "Paste" "paste" + add_menu_item "$itemPath\\030PasteDateCreated" "Paste 'Date Created'" "pastedc" + add_menu_item "$itemPath\\040PasteDateModified" "Paste 'Date Modified'" "pastedm" +} + +function add_menu_root() { + reg.exe add "$1" -v MUIVerb -d "$2" -f > /dev/null 2>&1 + reg.exe add "$1" -v SubCommands -d "" -f > /dev/null 2>&1 + reg.exe add "$1" -v Icon -d "$3" -f > /dev/null 2>&1 +} + +function add_menu_item() { + # key, label, arg + reg.exe add "$1" -ve -d "$2" -f > /dev/null 2>&1 + reg.exe add "$1\\command" -ve -d "\"$bashPath\" --login -i \"$scriptPath\" \"$3\" \"%1\"" -f > /dev/null 2>&1 +} + +function uninstall() { + echo "Uninstalling..." + uninstall_internal "$fRootKey" + uninstall_internal "$dRootKey" + rm -f "$clip_file" + echo "Done" +} + +function uninstall_internal() { + if reg.exe query "$1" > /dev/null 2>&1; then + echo "y" | reg.exe delete "$1" > /dev/null 2>&1 + fi +} + +##### context menu commands (copy/paste functions) + +function copy { + dc="$(powershell.exe -Command '(Get-Item '\"$1\"').CreationTime.ToString('\"$datetime_format\"')')" + dm="$(powershell.exe -Command '(Get-Item '\"$1\"').LastWriteTime.ToString('\"$datetime_format\"')')" + echo "File/Folder: $1" + echo "---" + echo "Date Created: $dc" + echo "Date Modified: $dm" + + echo "$dc" > "$clip_file" + echo "$dm" >> "$clip_file" + echo "---" + echo "Timestamps copied" +} + +function pastedc { + guard + dc_old="$(powershell.exe -Command '(Get-Item '\"$1\"').CreationTime.ToString('\"$datetime_format\"')')" + dm_old="$(powershell.exe -Command '(Get-Item '\"$1\"').LastWriteTime.ToString('\"$datetime_format\"')')" + dc_new=$(sed -n '1p' "$clip_file") + dm_new=$(sed -n '2p' "$clip_file") + echo "File/Folder: $1" + echo "---" + highlight_diff "Date Created: " "$dc_old" "$dc_new" + echo "---" + highlight_diff "Date Modified:" "$dm_old" "$dm_old" + echo "---" + read -p "Apply changes? (y/N) " yn + if [ "${yn,,}" = "y" ] + then + powershell.exe -Command "(Get-Item '$1').CreationTime=[datetime]::ParseExact('$dc_new', '$datetime_format', \$null)" + echo "Done" + else + echo "Canceled" + fi +} + +function pastedm { + guard + dc_old="$(powershell.exe -Command '(Get-Item '\"$1\"').CreationTime.ToString('\"$datetime_format\"')')" + dm_old="$(powershell.exe -Command '(Get-Item '\"$1\"').LastWriteTime.ToString('\"$datetime_format\"')')" + dc_new=$(sed -n '1p' "$clip_file") + dm_new=$(sed -n '2p' "$clip_file") + echo "File/Folder: $1" + echo "---" + highlight_diff "Date Created: " "$dc_old" "$dc_old" + echo "---" + highlight_diff "Date Modified:" "$dm_old" "$dm_new" + echo "---" + read -p "Apply changes? (y/N) " yn + if [ "${yn,,}" = "y" ] + then + powershell.exe -Command "(Get-Item '$1').LastWriteTime=[datetime]::ParseExact('$dm_new', '$datetime_format', \$null)" + echo "Done" + else + echo "Canceled" + fi +} + +function paste { + guard + dc_old="$(powershell.exe -Command '(Get-Item '\"$1\"').CreationTime.ToString('\"$datetime_format\"')')" + dm_old="$(powershell.exe -Command '(Get-Item '\"$1\"').LastWriteTime.ToString('\"$datetime_format\"')')" + dc_new=$(sed -n '1p' "$clip_file") + dm_new=$(sed -n '2p' "$clip_file") + echo "File/Folder: $1" + echo "---" + highlight_diff "Date Created: " "$dc_old" "$dc_new" + echo "---" + highlight_diff "Date Modified:" "$dm_old" "$dm_new" + echo "---" + read -p "Apply changes? (y/N) " yn + if [ "${yn,,}" = "y" ] + then + powershell.exe -Command "(Get-Item '$1').CreationTime=[datetime]::ParseExact('$dc_new', '$datetime_format', \$null)" + powershell.exe -Command "(Get-Item '$1').LastWriteTime=[datetime]::ParseExact('$dm_new', '$datetime_format', \$null)" + echo "Done" + else + echo "Canceled" + fi +} + +function guard() { + if ! [ -f "$clip_file" ]; then + echo "Timestamps clipboard empty." + read -p "Press any key to exit..." -n1 -s; echo + exit 0 + fi + + dc=$(sed -n '1p' "$clip_file") + dm=$(sed -n '2p' "$clip_file") + powershell.exe -Command "[datetime]::ParseExact('$dc', '$datetime_format', \$null)" > /dev/null && + powershell.exe -Command "[datetime]::ParseExact('$dm', '$datetime_format', \$null)" > /dev/null + + if [ ! $? -eq 0 ]; + then + echo "Timestamps clipboard corrupted. Copy new timestamps." + read -p "Press any key to exit..." -n1 -s; echo + exit 0 + fi +} + +function highlight_diff() { + local label="$1" + local old="$2" + local new="$3" + + local reset="\033[0m" + local green="\033[1;32m" + local changed=0 + + echo -e "$label $old (old)$reset" + + echo -n "$label " + + IFS='- :' + read -r old_y old_m old_d old_H old_M old_S <<< "$old" + read -r new_y new_m new_d new_H new_M new_S <<< "$new" + unset IFS + + color_part() { + local old_val="$1" + local new_val="$2" + if [[ "$old_val" == "$new_val" ]]; then + echo -ne "${new_val}${reset}" + else + echo -ne "${green}${new_val}${reset}" + changed=1 + fi + } + + color_part "$old_y" "$new_y" + echo -ne "-" + color_part "$old_m" "$new_m" + echo -ne "-" + color_part "$old_d" "$new_d" + echo -n " " + color_part "$old_H" "$new_H" + echo -ne ":" + color_part "$old_M" "$new_M" + echo -ne ":" + color_part "$old_S" "$new_S" + + if [[ "$changed" -eq 1 ]]; then + echo -e " ${green}(new)${reset}" + else + echo -e " (new)${reset}" + fi +} + +main "$@"