From 2f259ca60e1173f126e4e91321c86ae8b1612bc2 Mon Sep 17 00:00:00 2001 From: redcatbaer Date: Mon, 31 Mar 2025 10:18:54 +0200 Subject: [PATCH 1/5] #1: First working version of the sync. --- .gitignore | 12 ++++ .idea/.gitignore | 8 +++ README.md | 53 +++++++++++++++- doc/changes/changelog.md | 4 ++ doc/changes/changes_1.0.0.md | 13 ++++ doc/design.md | 117 ++++++++++++++++++++++++++++++++++ doc/system_requirements.md | 66 ++++++++++++++++++++ lips.sh | 118 +++++++++++++++++++++++++++++++++++ tools/shellcheck.sh | 8 +++ 9 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 doc/changes/changelog.md create mode 100644 doc/changes/changes_1.0.0.md create mode 100644 doc/design.md create mode 100644 doc/system_requirements.md create mode 100755 lips.sh create mode 100755 tools/shellcheck.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1270bc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# JetBrains IDE +.idea/ +*.iml +*.iws + +*.gz +*.tar +*.zip +*.log +*.lock +~* +*.bak \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/README.md b/README.md index 93483ba..9893bbc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ -# linux-iphone-photo-sync -Automation for loading images from an iPhone under Linux +# Linux iPhone Photo Sync (LIPS) + +This project provides a small Bash script that does an incremental synchronization of the photos on your iPhone to a Linux machine. + +It also exports all photos from HEIC to JPEG format in a separate directory. + +## Features + +1. Incremental sync from iPhone photos to a local directory +2. Export of all HEIC images to JPEG +3. Fully automatic operation + +## Installation + +Before you can run the script, please install the following packages: + +```shell +sudo apt install libimobiledevice6 libimobiledevice-utils imagemagick +``` + +## Running the Script + +You simply run the script without any parameters. + +```shell +./lips.sh +``` + +The script will create the following directories: + +| Directory | Purpose | +|----------------------------------------|------------------------------------------------------------------| +| `~/mnt/iPhone` | Mount point for the iPhone (source from where to copy the files) | +| `/iPhone/original_photos` | Directory to which the original photos are synched | +| `/iPhone/exported_jpgs` | Target directory for JPGs convered from HEIC | + +## Developer Information + +If you want to build the project, please install the following additional packages + +```shell +sudo apt install shellcheck +``` + +### Static Code Analysis + +To run static code analysis, please execute: + +```shell +tools/shellcheck.sh +``` \ No newline at end of file diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md new file mode 100644 index 0000000..82b1d33 --- /dev/null +++ b/doc/changes/changelog.md @@ -0,0 +1,4 @@ +# Changelog + +* [1.0.0](changes_1.0.0.md +* ) \ No newline at end of file diff --git a/doc/changes/changes_1.0.0.md b/doc/changes/changes_1.0.0.md new file mode 100644 index 0000000..d2f9ddf --- /dev/null +++ b/doc/changes/changes_1.0.0.md @@ -0,0 +1,13 @@ +# Linux iPhone Photo Sync (LIPS) 1.0.0, released 2024-07-24 + +Code name: Foundational Photo Sync + +## Summary + +In this release, our focus has been on providing the main functionalities of LIPS: photo synchronization and JPEG conversion. LIPS now enables Linux users to sync images from an iPhone via USB and converts any HEIC images to the more universally supported JPEG format. + +Please note that while the application has undergone thorough testing, it's always possible for unforeseen issues to arise depending on specific system configurations and usage patterns. As always, we appreciate your feedback as we strive to make LIPS a reliable tool for iPhone and Linux users. + +## Features + +#1: Introduced one-way photo synchronization from an iPhone to a Linux machine. \ No newline at end of file diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 0000000..7a89116 --- /dev/null +++ b/doc/design.md @@ -0,0 +1,117 @@ +# System Design Specification - Linux iPhone Photo Sync (LIPS) + +## Introduction + +This document presents the design decision made in LIPSync which meet the requirements outlined in system_requirements.md. + +## Structural View + +### Debian Linux Dependencies Only + +To ensure compatibility and mitigate dependency issues, LIPSync is designed to use only tools and libraries available on Debian 12 or derivatives like Ubuntu. + +#### Required Packages +`dsn~required-packages~1` + +LIPS requires the following packages. All of them are available on a vanilla Debian-based system: + +| Package | Purpose | +|--------------------------|-----------------------------------| +| `libimobiledevice6` | Library to access iPhones / iPads | +| `libimobiledevice-utils` | Binaries to use iPhone functions | +| `imagemagick` | Image conversion | + +Covers: + +* `req~debian-linux-dependencies-only~1` + +## Runtime View + +## Incremental Photo Download + +LIPS incorporates an incremental photo download feature. This means it skips downloading photos that already exist in the target directory. This approach optimizes the download process by avoiding unnecessary duplication and facilitating the resumption of interrupted downloads. + +### RSync for DCIM Directory +`dsn~rsync-for-dcim-directory~1` + +LIPS uses `rsync` to incrementally download images from the iPhones `DCIM` directory. + +Covers: + +* `req~incremental-photo-download~1` + +### iPhone Mount +`dsn~iphone-mount~1` + +LIPS mounts the iPhone via iFuse under `/mnt/iPhone`. + +Covers: + +* `req~incremental-photo-download~1` + +### Picture Directory +`dsn~picture-directory~1` + +LIPS reads the user picture directory from the user config. + +Covers: + +* `req~incremental-photo-download~1` + +### Fallback Picture Directory +`dsn~fallback-picture-directory~1` + +If the picture directory is not given in the user config, LIPS falls back to `/Pictures`. + +Covers: + +* `req~incremental-photo-download~1` + +### Original Photos Directory +`dsn~original-photos-directory~1` + +LIPS syncs the original content of the iPhones `DCIM` directory to `/original_photos`. + +Covers: + +* `req~incremental-photo-download~1` + +## JPEG Conversion + +Similarly, the script is designed to incrementally convert HEIC files to JPEG. It checks if a JPEG copy of the HEIC file already exists before carrying out the conversion. If the JPEG copy exists, the conversion process is skipped. This design approach optimizes CPU usage and processing time. + +### Check JPEG Existence in Target Directory +`dsn~check-jpeg-existence-in-target-directory~1` + +LIPS checks if the JPEG already exists in the target directory. If it does, LIPS skips the export. + +Covers: + +* `req~jpeg-incremental-conversion~1` + +### Convert HEIC to JPEG With ImageMagick +`dsn~convert-heic-to-jpeg-with-imagemagick~1` + +LIPS uses the `convert` tool from the ImageMagick suite to convert images from HEIC to JPG. + +Covers: + +* `req~jpeg-incremental-conversion~1` + +### Fixed JPEG Quality of 85% +`dsn~fixed-jpeg-quality-of-85-percent~1` + +LIPS converts HEIC to JPEGs at a fixed quality of 85%. + +Covers: + +* `req~jpeg-incremental-conversion~1` + +### JPEG Export Directory +`dsn~jpeg-export-directory~1` + +LIPS exports JPEGs to `/exported_jpgs`. + +Covers: + +* `req~jpeg-incremental-conversion~1` \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md new file mode 100644 index 0000000..2f9ba8a --- /dev/null +++ b/doc/system_requirements.md @@ -0,0 +1,66 @@ +# Linux iPhone Photo Sync (LIPS) + +## Terms and Abbreviations + +###### High Efficiency Image Coding (HEIC) + +The HEIC is a file format used for storing images. It provides higher image quality and compression than traditional formats like JPG. It's widely used in Apple's IOS and macOS devices but isn't universally supported on all platforms. + +## Features + +### Photo Synchronization +`feat~photo-synchronization~1` + +LIPS does a one-way sync of images from an iPhone via USB onto a Linux machine. + +Needs: req + +### JPEG Conversion +`feat~jpeg-conversion~1` + +LIPSync converts the downloaded HEIC images to JPEG format. + +Needs: req + +## High-level Requirements + +### Photo Synchronization + +#### Debian Linux Dependencies Only +`req~debian-linux-dependencies-only~1` + +LIPS requires only tools and libraries which are available on a Debian 12 or derived (e.g. Ubuntu) system. + +Covers: + +* [`feat~photo-synchronization~1`](#photo-synchronization) + +Needs: dsn + +#### Incremental Photo Download +`req~incremental-photo-download~1` + +LIPS downloads only photos that are not yet in the target directory. + +Rationale: + +This speeds up the download process and makes restarting after interrupted download more reliable. + +Covers: + +* [`feat~photo-synchronization~1`](#photo-synchronization) + +Needs: dsn + +### JPG Conversion + +#### JPEG Incremental Conversion +`req~jpeg-incremental-conversion~1` + +LIPS converts all HEIC files for which no JPEG exists into a JPEG file in a separate export directory. + +Covers: + +* [`feat~jpeg-conversion~1`](#jpeg-conversion) + +Needs: dsn \ No newline at end of file diff --git a/lips.sh b/lips.sh new file mode 100755 index 0000000..d1402a3 --- /dev/null +++ b/lips.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Get the standard user directory for pictures. +# Falls back to "Pictures" if not set. +if [ -f "$HOME/.config/user-dirs.dirs" ]; then + # shellcheck disable=SC1091 + source "$HOME/.config/user-dirs.dirs" + pictures_dir="$XDG_PICTURES_DIR" +else + pictures_dir="$HOME/Pictures" +fi + +# Set source and target paths. +readonly iphone_fuse_dir="$HOME/mnt/iPhone" +readonly photo_source_dir="$iphone_fuse_dir/DCIM" +readonly photo_target_dir="$pictures_dir/iPhone/original_photos" +readonly jpg_export_dir="$pictures_dir/iPhone/exported_jpgs" + +# General constants +readonly jpeg_quality=85 +readonly exit_software=1 +readonly exit_ok=0 + +create_directories_if_missing() { + for dir in "${iphone_fuse_dir}" "${photo_target_dir}" "${jpg_export_dir}"; do + if [ ! -d "${dir}" ]; then + echo "${dir} does not exist. Creating..." + mkdir --parents "${dir}" + fi +done +} + +pair_iphone() { + # Get the device's unique Device ID: + udid=$(idevice_id -l) + + # Check if the device is paired: + if idevicepair -u "${udid}" validate >/dev/null 2>&1; then + echo "iPhone is already paired: ${udid}" + else + echo "iPhone is not paired. Attempting to pair..." + idevicepair -u "${udid}" pair + + # Validate if pairing was successful: + if idevicepair -u "${udid}" validate >/dev/null 2>&1; then + echo "Pairing successful: ${udid}" + else + echo "Pairing failed. Please unplug and replug the device, or restart the device." + fi + fi +} + +mount_iphone() { + if mountpoint -q "${iphone_fuse_dir}"; then + echo "iPhone is already mounted at \"${iphone_fuse_dir}\"." + else + echo "Attempting to mount iPhone at \"${iphone_fuse_dir}\"." + if ifuse "${iphone_fuse_dir}"; then + echo "iPhone mounted at ${iphone_fuse_dir}" + else + echo "Failed to mount iPhone. Please make sure it is plugged in and the screen is unlocked." + fi + fi +} + +sync_photos() { + echo "Synchronizing photos from \"$photo_source_dir\" to \"$photo_target_dir\"..." + rsync --archive --progress --human-readable --delete "$photo_source_dir/" "$photo_target_dir/" +} + +export_to_jpeg() { + echo "Exporting HEIC from \"$photo_target_dir\" to JPEGs \"$jpg_export_dir\"..." + trap "echo 'Exporting to JPEG interrupted. Exiting...'; exit 1;" SIGINT SIGTERM + + while IFS= read -r -d '' source_file; do + rel_path=$(realpath --relative-to="$photo_target_dir" "$source_file") + dir_path=$(dirname "$rel_path") + mkdir -p "$jpg_export_dir/$dir_path" + base_name=$(basename "$source_file" .HEIC) + target_file="${jpg_export_dir}/${dir_path}/${base_name}.jpg" + if [ ! -e "${target_file}" ]; then + echo "Converting \"${source_file}\" to \"${target_file}\"." + convert -quality "${jpeg_quality}" "${source_file}" "${target_file}" + # preserve timestamp + touch -r "${source_file}" "${target_file}" + fi + done < <(find "${photo_target_dir}" -type f -iname '*.HEIC' -print0) +} + +sync_existing_jpgs() { + echo "Exporting HEIC from \"$photo_target_dir\" to JPEGs \"$jpg_export_dir\"..." + rsync -m --include='*.jpg' --include='*.JPG' --include='*.jpeg' --include="*,JPEG" \ + --exclude='*' -a --progress "$photo_target_dir/" "$jpg_export_dir/" +} + +unmount_iphone() { + if mountpoint -q "${iphone_fuse_dir}"; then + echo "Unmounting iPhone from \"${iphone_fuse_dir}\"." + if umount "${iphone_fuse_dir}"; then + echo "Successfully unmounted iPhone." + else + echo "Failed to unmount iPhone. You might need to sudo or check if other processes are using files within the mounted directory." + fi + else + echo "iPhone is already unmounted." + fi +} + +create_directories_if_missing && +pair_iphone && +mount_iphone && +sync_photos && +export_to_jpeg && +unmount_iphone || exit "$exit_software" + +exit "$exit_ok" \ No newline at end of file diff --git a/tools/shellcheck.sh b/tools/shellcheck.sh new file mode 100755 index 0000000..64f66e6 --- /dev/null +++ b/tools/shellcheck.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +base_dir="$(cd "$(dirname "$0")/.." >/dev/null 2>&1 ; pwd -P)" +readonly base_dir + +find "$base_dir" -name '*.sh' -type f -print0 | xargs -0 -n1 shellcheck -x \ No newline at end of file From bbca92442987851bcb00ef30b5c2784b33900404 Mon Sep 17 00:00:00 2001 From: redcatbaer Date: Mon, 31 Mar 2025 10:23:54 +0200 Subject: [PATCH 2/5] #1: Added CI build --- .github/workflows/ci_build.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/ci_build.yml diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml new file mode 100644 index 0000000..ea22b72 --- /dev/null +++ b/.github/workflows/ci_build.yml @@ -0,0 +1,18 @@ +name: ShellCheck + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Run ShellCheck + run: bash tools/shellcheck.sh \ No newline at end of file From 6dedd6c889a1c7e14048cf764786cfb3fd1450cc Mon Sep 17 00:00:00 2001 From: redcatbaer Date: Mon, 31 Mar 2025 10:26:23 +0200 Subject: [PATCH 3/5] #1: Added dependencies to CI build --- .github/workflows/ci_build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index ea22b72..bbfe4eb 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -11,6 +11,11 @@ jobs: runs-on: ubuntu-latest steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ibimobiledevice6 libimobiledevice-utils imagemagick + - name: Check out code uses: actions/checkout@v4 From ac5c890a37f702f760aa808fc4723f5f70f40818 Mon Sep 17 00:00:00 2001 From: redcatbaer Date: Mon, 31 Mar 2025 11:35:58 +0200 Subject: [PATCH 4/5] #1: Fixed shellcheck warning. --- .github/workflows/ci_build.yml | 7 +------ doc/design.md | 9 +++++++++ lips.sh | 7 ++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index bbfe4eb..278738a 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -8,14 +8,9 @@ on: jobs: check: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y ibimobiledevice6 libimobiledevice-utils imagemagick - - name: Check out code uses: actions/checkout@v4 diff --git a/doc/design.md b/doc/design.md index 7a89116..3a81703 100644 --- a/doc/design.md +++ b/doc/design.md @@ -114,4 +114,13 @@ LIPS exports JPEGs to `/exported_jpgs`. Covers: +* `req~jpeg-incremental-conversion~1` + +### Copy Existing JPEGs +`dsn~copy-existing-jpegs~1` + +LIPS uses `rsync` to copy existing JPEGs to the export directory. + +Covers: + * `req~jpeg-incremental-conversion~1` \ No newline at end of file diff --git a/lips.sh b/lips.sh index d1402a3..96f1e52 100755 --- a/lips.sh +++ b/lips.sh @@ -99,7 +99,7 @@ unmount_iphone() { if mountpoint -q "${iphone_fuse_dir}"; then echo "Unmounting iPhone from \"${iphone_fuse_dir}\"." if umount "${iphone_fuse_dir}"; then - echo "Successfully unmounted iPhone." + echo "Successfully unmounted iPhone. You can unplug it now." else echo "Failed to unmount iPhone. You might need to sudo or check if other processes are using files within the mounted directory." fi @@ -112,7 +112,8 @@ create_directories_if_missing && pair_iphone && mount_iphone && sync_photos && -export_to_jpeg && -unmount_iphone || exit "$exit_software" +unmount_iphone && +sync_existing_jpgs && +export_to_jpeg || exit "$exit_software" exit "$exit_ok" \ No newline at end of file From ce1dd7ceeb054bd96a1d404f4fd779cb9d49c14a Mon Sep 17 00:00:00 2001 From: redcatbaer Date: Mon, 31 Mar 2025 11:49:21 +0200 Subject: [PATCH 5/5] #1: Added a check that allows skipping to JPEG export if iPhone is not connected. --- lips.sh | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lips.sh b/lips.sh index 96f1e52..10fc0e8 100755 --- a/lips.sh +++ b/lips.sh @@ -66,12 +66,12 @@ mount_iphone() { } sync_photos() { - echo "Synchronizing photos from \"$photo_source_dir\" to \"$photo_target_dir\"..." + echo "Synchronizing photos from \"$photo_source_dir\" to \"$photo_target_dir\" ..." rsync --archive --progress --human-readable --delete "$photo_source_dir/" "$photo_target_dir/" } export_to_jpeg() { - echo "Exporting HEIC from \"$photo_target_dir\" to JPEGs \"$jpg_export_dir\"..." + echo "Exporting HEIC from \"$photo_target_dir\" to JPEGs \"$jpg_export_dir\" ..." trap "echo 'Exporting to JPEG interrupted. Exiting...'; exit 1;" SIGINT SIGTERM while IFS= read -r -d '' source_file; do @@ -90,7 +90,7 @@ export_to_jpeg() { } sync_existing_jpgs() { - echo "Exporting HEIC from \"$photo_target_dir\" to JPEGs \"$jpg_export_dir\"..." + echo "Synchronizing existing JPEGS from \"$photo_target_dir\" to JPEGs \"$jpg_export_dir\" ..." rsync -m --include='*.jpg' --include='*.JPG' --include='*.jpeg' --include="*,JPEG" \ --exclude='*' -a --progress "$photo_target_dir/" "$jpg_export_dir/" } @@ -98,7 +98,7 @@ sync_existing_jpgs() { unmount_iphone() { if mountpoint -q "${iphone_fuse_dir}"; then echo "Unmounting iPhone from \"${iphone_fuse_dir}\"." - if umount "${iphone_fuse_dir}"; then + if fusermount -u "${iphone_fuse_dir}"; then echo "Successfully unmounted iPhone. You can unplug it now." else echo "Failed to unmount iPhone. You might need to sudo or check if other processes are using files within the mounted directory." @@ -108,11 +108,17 @@ unmount_iphone() { fi } -create_directories_if_missing && -pair_iphone && -mount_iphone && -sync_photos && -unmount_iphone && +create_directories_if_missing || exit "$exit_software" + +if idevice_id -l >/dev/null 2>&1; then + pair_iphone && + mount_iphone && + sync_photos && + unmount_iphone || exit "$exit_software" +else + echo "Currently no iPhone seems to be connected. Skipping ahead to JPEG conversion of already synchronized photos." +fi + sync_existing_jpgs && export_to_jpeg || exit "$exit_software"