Initial commit: iCloud Drive GTK3 GUI for ElementaryOS
- Dashboard with live sync status, current file indicator, auto-refresh - Logs page with color-coded ERROR/WARN/INFO lines and live follow - Photos tab: download recent photos as JPEG via icloudpd - Settings with save + optional service restart - Install/uninstall scripts and .desktop entry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
21be796265
6 files changed
+1935
No files matched your search
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
@@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Name=iCloud Drive
|
||||
Comment=Sync iCloud Drive on Linux
|
||||
Exec=python3 /home/nateemmert/Documents/icloud-gui/icloud_gui.py
|
||||
Icon=folder-cloud
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;FileManager;
|
||||
Keywords=iCloud;Apple;Sync;Drive;
|
||||
StartupNotify=true
|
||||
StartupWMClass=icloud_gui
|
||||
+1351
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
iCloud Photos FUSE filesystem — uses fuse-python (same library as icloud-linux).
|
||||
|
||||
Directory layout mirrors icloudpd output: /YYYY/MM/DD/filename.HEIC
|
||||
- Listing: instant (index from icloudpd --only-print-filenames)
|
||||
- Open: downloads on demand via icloudpd, cached in ~/.cache/icloud-photos/
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import errno
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
import fuse
|
||||
from fuse import Fuse
|
||||
|
||||
if not hasattr(fuse, "__version__"):
|
||||
fuse.__version__ = "0.2"
|
||||
fuse.fuse_python_api = (0, 2)
|
||||
|
||||
log = logging.getLogger("icloud-photos-fs")
|
||||
|
||||
CACHE_DIR = Path.home() / ".cache" / "icloud-photos"
|
||||
ICLOUDPD = str(Path(__file__).parent.parent / "icloud-linux" / "icloud-linux" / ".venv" / "bin" / "icloudpd")
|
||||
# Fallback paths
|
||||
for _candidate in [
|
||||
Path.home() / "icloud-linux" / "icloud-linux" / ".venv" / "bin" / "icloudpd",
|
||||
Path.home() / "icloud-linux" / ".venv" / "bin" / "icloudpd",
|
||||
]:
|
||||
if _candidate.exists():
|
||||
ICLOUDPD = str(_candidate)
|
||||
break
|
||||
|
||||
|
||||
class MyStat(fuse.Stat):
|
||||
def __init__(self):
|
||||
self.st_mode = 0
|
||||
self.st_ino = 0
|
||||
self.st_dev = 0
|
||||
self.st_nlink = 0
|
||||
self.st_uid = os.getuid()
|
||||
self.st_gid = os.getgid()
|
||||
self.st_size = 0
|
||||
self.st_atime = 0
|
||||
self.st_mtime = 0
|
||||
self.st_ctime = 0
|
||||
|
||||
|
||||
class FileEntry:
|
||||
__slots__ = ("rel_path", "mtime")
|
||||
|
||||
def __init__(self, rel_path: str, mtime: float = 0.0):
|
||||
self.rel_path = rel_path
|
||||
self.mtime = mtime
|
||||
|
||||
@property
|
||||
def cached(self) -> Path:
|
||||
return CACHE_DIR / self.rel_path.lstrip("/")
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
cp = self.cached
|
||||
if cp.exists():
|
||||
return cp.stat().st_size
|
||||
# Return a large placeholder so callers don't truncate reads before download
|
||||
return 8 * 1024 * 1024 # 8 MB
|
||||
|
||||
|
||||
class ICloudPhotosFS(Fuse):
|
||||
def __init__(self, username, password, cookie_dir, library,
|
||||
on_progress=None, on_done=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._cookie_dir = cookie_dir
|
||||
self._library = library
|
||||
self._on_progress = on_progress
|
||||
self._on_done = on_done
|
||||
self._lock = threading.RLock()
|
||||
self._mount_time = time.time()
|
||||
|
||||
# rel_path → FileEntry (key has leading /)
|
||||
self._files: dict[str, FileEntry] = {}
|
||||
# "2024/06/12" → set of filenames
|
||||
self._dirs: dict[str, set] = defaultdict(set)
|
||||
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def fsinit(self):
|
||||
"""Called after mount — start indexing, then prefetch recent photos."""
|
||||
threading.Thread(target=self._start_tasks, daemon=True).start()
|
||||
|
||||
def _start_tasks(self):
|
||||
self._build_index()
|
||||
# Prefetch BEFORE signaling done so Photos sees real cached files immediately
|
||||
self._prefetch_recent(200)
|
||||
# Now tell the GUI we're ready
|
||||
if self._on_done:
|
||||
self._on_done(getattr(self, "_index_count", 0))
|
||||
|
||||
def _prefetch_recent(self, count: int):
|
||||
log.info(f"Prefetching {count} most recent photos…")
|
||||
cmd = [
|
||||
ICLOUDPD,
|
||||
"--username", self._username,
|
||||
"--password", self._password,
|
||||
"--cookie-directory", self._cookie_dir,
|
||||
"--library", self._library,
|
||||
"--no-progress-bar",
|
||||
"--directory", str(CACHE_DIR),
|
||||
"--recent", str(count),
|
||||
"--convert-all-heic-to-jpeg",
|
||||
]
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=600)
|
||||
log.info("Prefetch done")
|
||||
except Exception as e:
|
||||
log.error(f"Prefetch error: {e}")
|
||||
|
||||
# ── Index ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_index(self):
|
||||
log.info("Building index via icloudpd…")
|
||||
count = 0
|
||||
tmp_root = str(CACHE_DIR / "_index_root")
|
||||
Path(tmp_root).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cmd = [
|
||||
ICLOUDPD,
|
||||
"--username", self._username,
|
||||
"--password", self._password,
|
||||
"--cookie-directory", self._cookie_dir,
|
||||
"--library", self._library,
|
||||
"--no-progress-bar",
|
||||
"--directory", tmp_root,
|
||||
"--only-print-filenames",
|
||||
]
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, text=True, bufsize=1)
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rel = os.path.relpath(line, tmp_root).replace(os.sep, "/")
|
||||
except ValueError:
|
||||
continue
|
||||
if rel.startswith("..") or not rel:
|
||||
continue
|
||||
parts = rel.split("/")
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
|
||||
filename = parts[-1]
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
|
||||
# Skip video sidecars — Photos app doesn't want .MOV/.MP4 here
|
||||
if ext in (".mov", ".mp4", ".m4v", ".avi"):
|
||||
continue
|
||||
|
||||
# Present HEIC as .jpg (we serve converted JPEG bytes)
|
||||
if ext in (".heic", ".heif"):
|
||||
filename = os.path.splitext(filename)[0] + ".jpg"
|
||||
parts = parts[:-1] + [filename]
|
||||
rel = "/".join(parts)
|
||||
|
||||
fuse_path = "/" + rel
|
||||
dir_key = "/".join(parts[:-1])
|
||||
|
||||
with self._lock:
|
||||
if fuse_path not in self._files:
|
||||
# Store original rel_path so _download knows the real date path
|
||||
self._files[fuse_path] = FileEntry(fuse_path)
|
||||
self._dirs[dir_key].add(filename)
|
||||
for depth in range(1, len(parts)):
|
||||
_ = self._dirs["/".join(parts[:depth])]
|
||||
count += 1
|
||||
if count % 200 == 0 and self._on_progress:
|
||||
self._on_progress(count)
|
||||
proc.wait()
|
||||
except Exception as e:
|
||||
log.error(f"Index failed: {e}")
|
||||
|
||||
log.info(f"Index done: {count}")
|
||||
# Don't call on_done yet — prefetch runs first in _start_tasks
|
||||
self._index_count = count
|
||||
|
||||
# ── FUSE ops ───────────────────────────────────────────────────────────────
|
||||
|
||||
def getattr(self, path):
|
||||
st = MyStat()
|
||||
t = int(self._mount_time)
|
||||
|
||||
if path == "/":
|
||||
st.st_mode = stat.S_IFDIR | 0o555
|
||||
st.st_nlink = 2
|
||||
st.st_atime = st.st_mtime = st.st_ctime = t
|
||||
return st
|
||||
|
||||
with self._lock:
|
||||
if path in self._files:
|
||||
entry = self._files[path]
|
||||
st.st_mode = stat.S_IFREG | 0o444
|
||||
st.st_nlink = 1
|
||||
st.st_size = entry.size
|
||||
st.st_atime = st.st_mtime = st.st_ctime = int(entry.mtime or t)
|
||||
return st
|
||||
dir_key = path.lstrip("/")
|
||||
if dir_key in self._dirs:
|
||||
st.st_mode = stat.S_IFDIR | 0o555
|
||||
st.st_nlink = 2
|
||||
st.st_atime = st.st_mtime = st.st_ctime = t
|
||||
return st
|
||||
|
||||
return -errno.ENOENT
|
||||
|
||||
def readdir(self, path, offset):
|
||||
yield fuse.Direntry(".")
|
||||
yield fuse.Direntry("..")
|
||||
dir_key = path.lstrip("/")
|
||||
seen = set()
|
||||
|
||||
with self._lock:
|
||||
# Sub-directories
|
||||
for k in list(self._dirs.keys()):
|
||||
if dir_key == "":
|
||||
top = k.split("/")[0]
|
||||
if top not in seen:
|
||||
seen.add(top)
|
||||
yield fuse.Direntry(top)
|
||||
elif k.startswith(dir_key + "/"):
|
||||
rest = k[len(dir_key) + 1:]
|
||||
child = rest.split("/")[0]
|
||||
if child not in seen:
|
||||
seen.add(child)
|
||||
yield fuse.Direntry(child)
|
||||
# Files
|
||||
for fp in list(self._files.keys()):
|
||||
fp_dir = "/".join(fp.lstrip("/").split("/")[:-1])
|
||||
if fp_dir == dir_key:
|
||||
yield fuse.Direntry(fp.split("/")[-1])
|
||||
|
||||
# Per-date download locks so concurrent reads for same day don't double-download
|
||||
_dl_locks: dict = {}
|
||||
_dl_locks_mutex = threading.Lock()
|
||||
_dl_dates_done: set = set() # dates already fully downloaded
|
||||
|
||||
def _get_dl_lock(self, key):
|
||||
with self._dl_locks_mutex:
|
||||
if key not in self._dl_locks:
|
||||
self._dl_locks[key] = threading.Lock()
|
||||
return self._dl_locks[key]
|
||||
|
||||
def _ensure_downloaded(self, path: str, entry: "FileEntry"):
|
||||
"""Download the file's day-batch if not already cached."""
|
||||
if entry.cached.exists():
|
||||
return
|
||||
parts = path.lstrip("/").split("/")
|
||||
try:
|
||||
date_str = f"{parts[0]}-{parts[1]}-{parts[2]}"
|
||||
except IndexError:
|
||||
date_str = None
|
||||
lock_key = date_str or path
|
||||
with self._get_dl_lock(lock_key):
|
||||
if entry.cached.exists():
|
||||
return # another thread beat us to it
|
||||
if date_str and date_str in self._dl_dates_done:
|
||||
return # already downloaded this day
|
||||
self._download(path, entry, date_str)
|
||||
if date_str:
|
||||
self._dl_dates_done.add(date_str)
|
||||
|
||||
def open(self, path, flags):
|
||||
with self._lock:
|
||||
entry = self._files.get(path)
|
||||
if entry is None:
|
||||
return -errno.ENOENT
|
||||
write_flags = os.O_WRONLY | os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_APPEND
|
||||
if flags & write_flags:
|
||||
return -errno.EROFS
|
||||
if not entry.cached.exists():
|
||||
log.info(f"Downloading on open: {path}")
|
||||
self._ensure_downloaded(path, entry)
|
||||
return 0
|
||||
|
||||
def read(self, path, size, offset):
|
||||
with self._lock:
|
||||
entry = self._files.get(path)
|
||||
if entry is None:
|
||||
return -errno.ENOENT
|
||||
cp = entry.cached
|
||||
if not cp.exists():
|
||||
self._ensure_downloaded(path, entry)
|
||||
if not cp.exists():
|
||||
return b""
|
||||
try:
|
||||
with open(cp, "rb") as f:
|
||||
f.seek(offset)
|
||||
return f.read(size)
|
||||
except Exception:
|
||||
return b""
|
||||
|
||||
def _download(self, fuse_path: str, entry: "FileEntry", date_str=None):
|
||||
entry.cached.parent.mkdir(parents=True, exist_ok=True)
|
||||
cmd = [
|
||||
ICLOUDPD,
|
||||
"--username", self._username,
|
||||
"--password", self._password,
|
||||
"--cookie-directory", self._cookie_dir,
|
||||
"--library", self._library,
|
||||
"--no-progress-bar",
|
||||
"--directory", str(CACHE_DIR),
|
||||
"--convert-all-heic-to-jpeg",
|
||||
]
|
||||
if date_str:
|
||||
cmd += ["--skip-created-before", date_str,
|
||||
"--skip-created-after", date_str]
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=120)
|
||||
except Exception as e:
|
||||
log.error(f"Download error: {e}")
|
||||
|
||||
# Block writes
|
||||
def write(self, *_): return -errno.EROFS
|
||||
def mknod(self, *_): return -errno.EROFS
|
||||
def unlink(self, *_): return -errno.EROFS
|
||||
def mkdir(self, *_): return -errno.EROFS
|
||||
def rmdir(self, *_): return -errno.EROFS
|
||||
def rename(self, *_): return -errno.EROFS
|
||||
def symlink(self, *_): return -errno.EROFS
|
||||
def link(self, *_): return -errno.EROFS
|
||||
def chmod(self, *_): return -errno.EROFS
|
||||
def chown(self, *_): return -errno.EROFS
|
||||
def truncate(self, *_):return -errno.EROFS
|
||||
def utime(self, *_): return 0
|
||||
def statfs(self):
|
||||
st = fuse.StatVfs()
|
||||
st.f_bsize = 4096
|
||||
st.f_blocks = 0
|
||||
st.f_bfree = 0
|
||||
st.f_bavail = 0
|
||||
return st
|
||||
|
||||
|
||||
def mount(username, password, cookie_dir, mountpoint, library="PrimarySync",
|
||||
on_progress=None, on_done=None):
|
||||
if os.path.ismount(mountpoint):
|
||||
# -z = lazy unmount (detaches even if busy)
|
||||
os.system(f"fusermount -uz {mountpoint!r}")
|
||||
time.sleep(1)
|
||||
os.makedirs(mountpoint, exist_ok=True)
|
||||
|
||||
server = ICloudPhotosFS(
|
||||
username, password, cookie_dir, library,
|
||||
on_progress=on_progress, on_done=on_done,
|
||||
version="%prog 0.1",
|
||||
usage="iCloud Photos FUSE",
|
||||
dash_s_do="setsingle",
|
||||
)
|
||||
server.parse(args=[mountpoint, "-f"], errex=1)
|
||||
server.main()
|
||||
|
||||
|
||||
def unmount(mountpoint: str):
|
||||
os.system(f"fusermount -u {mountpoint!r}")
|
||||
Executable
+160
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
# iCloud Drive for Linux — installer
|
||||
# Installs icloud-linux (backend) + icloud-gui (GTK3 frontend) on ElementaryOS / Ubuntu / Debian.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://github.com/IsmaeelAkram/icloud-linux"
|
||||
INSTALL_DIR="$HOME/.local/lib/icloud-linux"
|
||||
GUI_INSTALL="$HOME/.local/lib/icloud-gui"
|
||||
BIN_DIR="$HOME/.local/bin"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
ICONS_DIR="$HOME/.local/share/icons/hicolor/scalable/apps"
|
||||
|
||||
BOLD="\033[1m"
|
||||
GREEN="\033[0;32m"
|
||||
YELLOW="\033[0;33m"
|
||||
CYAN="\033[0;36m"
|
||||
RED="\033[0;31m"
|
||||
RESET="\033[0m"
|
||||
|
||||
info() { echo -e "${CYAN} →${RESET} $*"; }
|
||||
success() { echo -e "${GREEN} ✓${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW} !${RESET} $*"; }
|
||||
die() { echo -e "${RED} ✗ ERROR:${RESET} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${BOLD}$*${RESET}"; }
|
||||
|
||||
# ── Detect distro ─────────────────────────────────────────────────────────────
|
||||
if ! command -v apt-get &>/dev/null; then
|
||||
die "This installer requires apt (Debian/Ubuntu/ElementaryOS). For other distros, install deps manually."
|
||||
fi
|
||||
|
||||
header "iCloud Drive for Linux — Installer"
|
||||
echo " Target install: $INSTALL_DIR"
|
||||
echo " GUI install: $GUI_INSTALL"
|
||||
echo ""
|
||||
|
||||
# ── System dependencies ───────────────────────────────────────────────────────
|
||||
header "1/5 Installing system dependencies"
|
||||
PKGS=(
|
||||
python3
|
||||
python3-venv
|
||||
python3-pip
|
||||
python3-gi
|
||||
python3-gi-cairo
|
||||
gir1.2-gtk-3.0
|
||||
python3-yaml
|
||||
fuse
|
||||
git
|
||||
libfuse2
|
||||
heif-gdk-pixbuf # HEIC/HEIF support for GNOME Photos
|
||||
)
|
||||
|
||||
# Elementary/Ubuntu 22+ ships libfuse3 but icloud-linux may need libfuse2
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y "${PKGS[@]}" 2>&1 | grep -E "(install|already)" || true
|
||||
success "Dependencies installed"
|
||||
|
||||
# ── Clone / update icloud-linux backend ───────────────────────────────────────
|
||||
header "2/5 Installing icloud-linux backend"
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
info "Already cloned — pulling latest"
|
||||
git -C "$INSTALL_DIR" pull --ff-only
|
||||
else
|
||||
info "Cloning $REPO_URL"
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# Set up Python venv and install requirements
|
||||
info "Setting up Python environment"
|
||||
python3 -m venv "$INSTALL_DIR/venv"
|
||||
"$INSTALL_DIR/venv/bin/pip" install -q --upgrade pip
|
||||
"$INSTALL_DIR/venv/bin/pip" install -q -r "$INSTALL_DIR/requirements.txt"
|
||||
success "Backend ready at $INSTALL_DIR"
|
||||
|
||||
# ── Install the GUI ───────────────────────────────────────────────────────────
|
||||
header "3/5 Installing iCloud Drive GUI"
|
||||
mkdir -p "$GUI_INSTALL"
|
||||
|
||||
# Copy the GUI script alongside this installer script
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "$SCRIPT_DIR/icloud_gui.py" ]; then
|
||||
cp "$SCRIPT_DIR/icloud_gui.py" "$GUI_INSTALL/icloud_gui.py"
|
||||
success "Copied GUI from $SCRIPT_DIR"
|
||||
else
|
||||
die "icloud_gui.py not found next to install.sh — make sure you run install.sh from the icloud-gui directory."
|
||||
fi
|
||||
|
||||
# ── Wrapper script in PATH ────────────────────────────────────────────────────
|
||||
header "4/5 Creating launcher"
|
||||
mkdir -p "$BIN_DIR"
|
||||
cat > "$BIN_DIR/icloud-drive" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
# Auto-generated by icloud-gui installer
|
||||
exec python3 "$GUI_INSTALL/icloud_gui.py" "\$@"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/icloud-drive"
|
||||
success "Launcher: $BIN_DIR/icloud-drive"
|
||||
|
||||
# Ensure ~/.local/bin is in PATH
|
||||
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||
warn "~/.local/bin is not in your PATH."
|
||||
echo " Add this to ~/.bashrc or ~/.profile and re-login:"
|
||||
echo ' export PATH="$HOME/.local/bin:$PATH"'
|
||||
fi
|
||||
|
||||
# ── Desktop entry & icon ──────────────────────────────────────────────────────
|
||||
mkdir -p "$DESKTOP_DIR" "$ICONS_DIR"
|
||||
|
||||
# Minimal cloud SVG icon (no external deps)
|
||||
cat > "$ICONS_DIR/com.elementary.icloud-linux.svg" <<'SVGEOF'
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#64a6e0"/>
|
||||
<stop offset="100%" stop-color="#1a6eb0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#g)"
|
||||
d="M50 30a14 14 0 0 0-26.5-6A10 10 0 1 0 14 44h36a10 10 0 0 0 0-14z"/>
|
||||
</svg>
|
||||
SVGEOF
|
||||
|
||||
cat > "$DESKTOP_DIR/com.elementary.icloud-linux.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=iCloud Drive
|
||||
Comment=Sync iCloud Drive on Linux
|
||||
Exec=$BIN_DIR/icloud-drive
|
||||
Icon=com.elementary.icloud-linux
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;FileManager;
|
||||
Keywords=iCloud;Apple;Sync;Drive;Cloud;
|
||||
StartupNotify=true
|
||||
StartupWMClass=icloud_gui
|
||||
EOF
|
||||
|
||||
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
|
||||
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
|
||||
success "App menu entry installed"
|
||||
|
||||
# ── Update GUI config to point at correct icloudctl ──────────────────────────
|
||||
# Patch the ICLOUDCTL path in the installed GUI script
|
||||
ICLOUDCTL_PATH="$INSTALL_DIR/icloudctl"
|
||||
sed -i "s|ICLOUDCTL = .*|ICLOUDCTL = \"$ICLOUDCTL_PATH\"|" "$GUI_INSTALL/icloud_gui.py"
|
||||
success "GUI configured to use $ICLOUDCTL_PATH"
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
header "Installation complete!"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Next steps:${RESET}"
|
||||
echo " 1. Open ${BOLD}iCloud Drive${RESET} from your app menu (Slingshot)"
|
||||
echo " or run: icloud-drive"
|
||||
echo ""
|
||||
echo " 2. Go to ${BOLD}Settings${RESET} and enter your Apple ID + password"
|
||||
echo ""
|
||||
echo " 3. Go to ${BOLD}Sign In${RESET} and authenticate (handles 2FA)"
|
||||
echo ""
|
||||
echo " 4. On ${BOLD}Dashboard${RESET}, press ${BOLD}Start${RESET} to begin syncing"
|
||||
echo ""
|
||||
echo " To uninstall, run: $SCRIPT_DIR/uninstall.sh"
|
||||
echo ""
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# iCloud Drive for Linux — uninstaller
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="$HOME/.local/lib/icloud-linux"
|
||||
GUI_INSTALL="$HOME/.local/lib/icloud-gui"
|
||||
BIN_DIR="$HOME/.local/bin"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
ICONS_DIR="$HOME/.local/share/icons/hicolor/scalable/apps"
|
||||
|
||||
echo "This will remove:"
|
||||
echo " $INSTALL_DIR (backend)"
|
||||
echo " $GUI_INSTALL (GUI)"
|
||||
echo " $BIN_DIR/icloud-drive"
|
||||
echo " $DESKTOP_DIR/com.elementary.icloud-linux.desktop"
|
||||
echo ""
|
||||
read -rp "Continue? [y/N] " ans
|
||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
||||
|
||||
# Stop the service first if running
|
||||
if command -v icloudctl &>/dev/null || [ -f "$INSTALL_DIR/icloudctl" ]; then
|
||||
"$INSTALL_DIR/icloudctl" stop 2>/dev/null || true
|
||||
fi
|
||||
|
||||
rm -rf "$INSTALL_DIR" "$GUI_INSTALL"
|
||||
rm -f "$BIN_DIR/icloud-drive"
|
||||
rm -f "$DESKTOP_DIR/com.elementary.icloud-linux.desktop"
|
||||
rm -f "$ICONS_DIR/com.elementary.icloud-linux.svg"
|
||||
|
||||
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
|
||||
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
|
||||
|
||||
echo "Uninstalled. Your config (~/.config/icloud-linux) and cache (~/.cache/icloud-linux) were left intact."
|
||||
echo "Remove them manually if you want a clean slate:"
|
||||
echo " rm -rf ~/.config/icloud-linux ~/.cache/icloud-linux"
|
||||
Reference in new issue
Block a user