Installing¶
As a container¶
docker run -d --name arrowloop -p 8422:8422 \
-v /mnt/user/appdata/arrowloop:/config \
-v /mnt/user:/data \
junkerderprovinz/arrowloop:latest
/config holds arrowloop.json, one state database per job, and the run log.
/config has to survive the container
Those state databases are what let the engine tell a new file from a deleted one. Lose them and every job forgets what the two sides agreed on and treats every file on both sides as new: nothing is destroyed, but a great deal is copied, and a deletion made while the record was missing is propagated to nobody.
A first start on an empty /config writes a starter configuration with no
jobs, so the interface comes up and can be edited rather than
crash-looping on a missing file.
A password for the interface¶
The container listens on every address, so anyone on the network who reaches port 8422 can start a job. Set a password under Settings, Security, and from then on every page but the login asks for it. Over plain HTTP the session cookie crosses the wire in clear, so a machine reachable from outside wants TLS in front of it either way; see The interface.
Until a password is set, the interface only answers when it is opened on an IP
address, localhost, a name without a dot such as tower, or a name ending in
.local, .lan, .home, .home.arpa, .internal, .localdomain or
.fritz.box. Otherwise a web page anywhere could point a name of its own at
this machine and use your browser to drive the interface. Any other name needs
the password, or has to be listed in ARROWLOOP_HOSTS, separated by commas; an
entry that starts with a dot, such as .example.com, covers every name under
it. Behind a reverse proxy, the name that counts is the one the proxy passes on.
Caddy and Traefik pass on the name the browser asked for. nginx passes on the
name in its proxy_pass line unless it is told proxy_set_header Host $host,
so behind a plain proxy_pass http://arrowloop:8422 every request through the
proxy gets in without a password; set one there. With or without a password,
the interface refuses a change that a page from another site started, and any
change not sent as JSON, which is what a page elsewhere could send without the
browser asking first.
Without a password, the interface also refuses to download or restore the settings backup or hand out an automatic one, to delete from the trash for good, to forget a job's state and to delete a target, since anybody who reaches the port could otherwise do it. The settings never hand out the Matrix token or the webhook address, with or without a password. The phone app and the desktop window, which nobody else reaches, are not asked.
The same section sets up a second factor: six digits from an authenticator app on top of the password. Setting it up shows eight recovery codes once, and each of them signs in a single time in place of a code.
It also registers passkeys, a key on a phone, a laptop or a security stick used
instead of typing the password. A browser offers one only on a host name over
HTTPS with a certificate it trusts, or on localhost, so on
http://192.168.1.5:8422 the section explains why it cannot. Behind a reverse
proxy with a real name they work, and each key belongs to the address it was
registered on. A key signs in by itself, so adding one asks for the password,
and for a code when the second factor is on. Changing or removing the password
removes every passkey, and so does a container that comes back with another
ARROWLOOP_PASSWORD_HASH.
All three are kept in /config/security.json, readable by its owner only, and
never in arrowloop.json. That file is downloaded whole as a settings backup and
replaced whole by a restore, so a hash in it would leave with every backup and
restoring an old one would switch the password off.
The password is stored as Argon2id over a value keyed with the install's key,
and the second factor's secret is sealed with it, so security.json on its own
is no use to whoever copies it. The key comes from ARROWLOOP_APP_KEY, 64
hexadecimal characters such as openssl rand -hex 32 prints, or without it from
/config/app.key, drawn on the first start. A copy of all of /config carries
that file too, so setting the variable keeps the key out of it. Keep the key:
under another one the password and the second factor no longer open, and the
container refuses to start until the old key is back or security.json is
deleted.
A forgotten password¶
At the foot of the sign-in form, Forgot password? has ArrowLoop write a
one-time code into its log: on Unraid under Docker, ArrowLoop, Logs, elsewhere
with docker logs arrowloop. The code stands on its own between the other
lines, in yellow. Type it and a new password on the same page. The code is
valid for ten minutes, and a wrong code does not spend the right one. After
five wrong codes from one address, that address waits a minute before any
code is checked again, the right one included. Asking again while it is valid
writes nothing, so the log does not fill up and the code you are typing stays
good. The new password takes the second factor, every passkey and every other
session with it, since any of them could belong to whoever made the reset
necessary, and the page says what went.
Whoever can read that log can reset the password this way: root on the host,
but also a log viewer such as Dozzle, any container given the Docker socket,
even read only, and any log shipper that forwards container output. If any of
those is less trusted than you, set the password with ARROWLOOP_PASSWORD_HASH
instead, which a code from the log cannot replace.
The environment variable¶
ARROWLOOP_PASSWORD_HASH still works, and it wins over the password set in the
interface, which then shows the password as set from outside and offers no form
for it. The hash is made under the install's key, so make it in the running
container and hand the result back:
docker exec -it arrowloop arrowloop hash-password -config /config/arrowloop.json
docker run -d --name arrowloop -p 8422:8422 \
-e ARROWLOOP_PASSWORD_HASH='argon2id$19456$2$1$...' \
...
The single quotes matter: the hash is full of $, which a shell would otherwise
read as variables. In a Compose file, write every $ as $$. A bcrypt hash,
as older versions printed and many other tools make, is still accepted.
Behind a reverse proxy¶
Five wrong passwords or reset codes from one address lock that address out for
a minute.
Behind a reverse proxy every request comes from the proxy, so one stranger
guessing would lock out everybody, you included. Set
ARROWLOOP_TRUSTED_PROXIES to the proxy's address, and the count goes against
the address the proxy reports in X-Forwarded-For instead. Several entries are
separated by commas. List only proxies you run: a machine on that list could
claim a new address for every guess.
When the proxy runs in another container, put the two on a Docker network of
their own, give the proxy a fixed address there, and list that one address.
Do not list a range such as 172.17.0.0/16 for Docker's default bridge. It
includes the bridge's gateway, 172.17.0.1, and connections that Docker's own
proxy relays to a published port arrive from that address, so anybody reaching
the port that way could choose the address their guesses count against.
If the authenticator app and the recovery codes are both gone, stop the
container and delete /config/security.json. That removes the password, the
second factor and the passkeys together. A damaged security.json stops the
container from starting rather than letting it come up without a password; the
log names the file, and deleting it has the same effect.
Jobs from other programs¶
A file manager such as CastleCommander can hand a pair of folders over as a new
job. Give the container a long random token in ARROWLOOP_API_TOKEN, for
example what openssl rand -hex 32 prints, and enter the same token in the
other program. It sends the token in the X-ArrowLoop-Token header and adds a
job with POST /api/jobs:
curl -X POST http://tower:8422/api/jobs \
-H 'Content-Type: application/json' \
-H 'X-ArrowLoop-Token: <token>' \
-d '{"name":"Filme nach Backup","left":"/data/Filme","right":"/data/Backup/Filme","direction":"leftToRight","mode":"mirror","schedule":"0 3 * * *"}'
The job takes the same fields as one in arrowloop.json. Without a state, it
gets state/<name>.db. A request with a name that is taken gets a 409, and
the job of that name stays as it is. before and after are dropped, as with
every save from the interface. The token opens only the job list and this one
route, never the settings or the trash, and a request with a wrong token is
refused even on an install without a password. A job added this way runs on its
schedule like any other, and a mirror deletes at its destination, so keep the
token as safe as the password.
On Unraid¶
ArrowLoop is in Community Applications:
search for ArrowLoop in the Apps tab and install it from there. The same
template is
templates/my-ArrowLoop.xml;
dropped into /boot/config/plugins/dockerMan/templates-user/ it appears in the
Docker tab's Add Container list, with every field editable. Most installs
set the password in the interface; the template's masked password hash field
is the environment variable above, and its text says how to fill it from the
container's console.
Mount the data path read-write. A two-way job writes to both sides by definition, and a read-only mount produces a job that fails on every run for a reason that reads like a bug.
As a desktop application¶
Windows, Linux and macOS builds are on the
releases page. Windows
and Linux get an installer. On Windows it gives ArrowLoop an entry under
Apps and installs it for all users under C:\Program Files\ArrowLoop, or in
another folder you pick. It asks for an administrator once, while it installs;
later updates ask nobody (see Updates). Two switches decide on a
Start menu entry and a desktop shortcut, both on at first, and both go to every
user on the computer. The next install, silent or not, starts from your answer
and removes a shortcut you left out. installer.exe /S installs without a
window, and /D=<folder> picks the folder for it.
The installer's first page also asks for a language. ArrowLoop opens in that language the first time, for everyone on the computer, until somebody picks another one inside the app.
The installer also cleans up after older versions. The copy 1.2.0 installed for
you alone under AppData\Local\Programs goes, with its shortcuts and its entry
under Apps, and so does an installation for all users from 1.1.0 or earlier. It
never touches your settings and jobs in %APPDATA%\ArrowLoop. A 1.2.0
installation that you leave alone keeps updating itself where it is; running
the new installer once moves it.
The installer asks a running ArrowLoop to quit, as Quit in the tray menu would,
so a sync under way stops cleanly. Its last page can start the new version as
the person signed in, and a copy that sat in the tray comes back there. A
computer with an ARM processor, such as a Snapdragon laptop, takes the one
named windows-arm64; the amd64 installer would run there too, but through
emulation and slower. The portable.exe beside it is the same program, run
from wherever you put it.
Linux gets the program as a single file, linux-amd64 for an Intel or AMD
processor and linux-arm64 for a 64-bit ARM one, and an installer for each.
It needs WebKitGTK 4.1 (libwebkit2gtk-4.1-0 on Debian and Ubuntu) for its
window. The installer is for you alone and asks for no password. It puts
ArrowLoop in ~/.local/share/com.junkerderprovinz.arrowloop and adds it to the
application menu, with a desktop icon if you want one. Right-clicking the menu
entry offers to uninstall it. That copy updates itself like the single file.
Make the download executable first
(chmod +x arrowloop-linux-amd64-installer).
It is not a client talking to a server. The scheduler, the run log and the API all live in the same process, and the window is a webview pointed at them. Nothing listens on the network at all, which is the difference between a desktop application and a server nobody asked to run.
The schedules run for as long as the window is open. For a machine that should
sync while nobody is looking, use arrowloop daemon and arrowloop service
instead; see the command line.
As a Flatpak¶
Each release also carries arrowloop-linux-amd64.flatpak and
arrowloop-linux-arm64.flatpak. Open the file with your software centre, or
install it in a terminal:
The Flatpak brings its own WebKitGTK and pulls the GNOME runtime from Flathub,
so nothing else needs installing. It never replaces itself, and the Info tile
says so where the other copies show Update automatically. A copy from the
file updates when you install the next release's file the same way. Its
settings and jobs live in
~/.var/app/io.github.junkerderprovinz.arrowloop/config/ArrowLoop.
The sandbox lets ArrowLoop reach your home folder, /media, /run/media and
/mnt, which covers your own files and most drives and network shares. A folder
anywhere else needs one command before a job can sync it:
Commands before and after a run run inside the sandbox too, so they find the tools the Flatpak ships rather than the ones on your system.
On Arch Linux and NixOS¶
On Arch Linux the single file and the installer work once WebKitGTK is installed:
NixOS keeps its libraries in the Nix store, not where the single file looks for
them, so the file does not start there. The Flatpak does: add
services.flatpak.enable = true; to your configuration, rebuild, and install
the file as shown above. The container runs on NixOS too.
Starting with the system¶
Start with the system, on the General tile under Settings, registers
ArrowLoop to start when you sign in. It is a per-user entry (a value under HKCU on Windows, a
.desktop file under ~/.config/autostart on Linux, a LaunchAgent in your own
Library on macOS), so it needs no administrator rights and affects nobody else on
the machine. The Flatpak asks the desktop for it through the Background portal,
which writes the entry for it, and GNOME may ask you once to allow it.
Started this way, ArrowLoop stays in the notification area, because nobody is waiting for a window when the session starts. It also comes back there after something outside ended it while its window was in the notification area, as an update does. Starting it by hand opens the window, and so does every start after you quit it.
The switch reads its state back from the system rather than from a settings file, so removing the entry with the Task Manager's own Startup tab turns the switch off too, instead of leaving it claiming something that has stopped being true.
Autostart on its own only gets the program running. A job that should sync
because the machine just came on wants runAtStart as well, or it will sit
there until its schedule is next due; see
the configuration reference.
The notification area¶
It puts an icon in the notification area. A click on it opens a small window beside the icon with what is running now, the last five runs, and buttons to pause, to sync everything and to open ArrowLoop. A second click or a click elsewhere closes it, and a double click opens the main window. The right-click menu has:
- Open, which brings the main window back.
- Force sync, which starts every job straight away, as if you had started each one by hand, so no pause and no battery setting holds it.
- Pause sync or Resume sync. A pause holds every scheduled and watching run until you resume, stops the runs already going, and stays after a restart. The icon turns grey with a pause badge while it lasts.
- Quit.
The menu and the small window speak the language the interface is set to.
The This computer card on the General tile decides what the window buttons do: whether closing quits or hides, and whether minimising goes to the taskbar or to the notification area. Closing quits by default, because a close button that quietly leaves a program running is the kind of surprise somebody finds a week later. Both choices switch off together with the icon, since a window that hides with nothing left to bring it back is gone. Switching the icon off or on applies at once.
Starting it a second time does not start a second copy. It brings the running window back, which is what a second double-click means when the first one is sitting in the notification area.
The shell is Wails v3, whose own notification area icon can carry a window. The icon is built at startup around the same PNG the window uses rather than committed as a second file, because a second file is one more thing to update when the logo changes, and forgetting it leaves the window and the notification area with different marks.
Of the three desktop builds, only the Windows one is tested by hand; the macOS and Linux builds are compiled but untested.
Updates¶
The desktop app updates itself from GitHub. It asks for the newest release,
downloads the file for your system in the background and checks it against the
release's checksums.txt. The new version starts the next time you start
ArrowLoop, so a sync that is running is never cut off, and a note in the corner
of the window says when it is ready. Pre-releases are never installed.
Update automatically, in the Version card on the Info tile under Settings,
turns this off; it is on from the start.
A container cannot replace itself and is updated the way it was deployed. There the same switch is off from the start and only reports: switched on, the container asks GitHub once a day for a newer release, names it under ArrowLoop in the Version card and writes it to its log. The phone updates through its store or a new APK, and the Flatpak through Flatpak (see As a Flatpak). Neither shows the switch.
The installed copy on Windows cannot replace itself, since no user may
write to its folder. A scheduled task named ArrowLoop Update does it instead,
once a day and five minutes after the computer starts, whether ArrowLoop is
open or not. It replaces ArrowLoop.exe, sets the version shown under Apps and
writes what it did, or why it did not, to %ProgramData%\ArrowLoop\update.log.
There is one installation, so the switch applies to everyone on the computer:
it lives in %ProgramData%\ArrowLoop\settings.json, which every user may
change, and the task skips its run while it is off. An open window notices
within a few minutes when the task has put a new version in place and shows
the note. The replaced program waits beside the new one as ArrowLoop.exe.old
until a later run removes it.
A portable copy, such as the arrowloop-windows-amd64-portable.exe from the
releases page, updates itself while it runs: a minute after it starts and once
a day after that, in its own folder, and it stays portable. The app on macOS
and Linux does the same. A folder it cannot write to, or a macOS app your
account cannot change, stays as it is. Its log is update.log beside the
configuration file.
Why the task runs as SYSTEM¶
Program Files belongs to the administrators. A program that replaces itself
there without asking anyone needs an account that may write there, and a
scheduled task under the system account is how Windows provides one; Firefox's
maintenance service and Chrome's updater task work the same way. The task
writes to three places and nowhere else: the installation folder, usually
C:\Program Files\ArrowLoop; the entry under Apps, in
HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\ArrowLoop; and
%ProgramData%\ArrowLoop, for its log. It reads nothing from any user's
profile. The folder under ProgramData belongs to the administrators, and only
settings.json in it is open to other users. If the installer cannot set the
folder up that way, it sets up no task. The installer gives the installation
folder the same rights as Program Files. Together that leaves no file a user
could swap for one the system account would run or write to.
You can install into another folder too. Many second drives let everyone
rename what sits at their top, and whoever can rename a folder above the
installation can swap it. On such a drive the task runs as the person who
installed ArrowLoop, without administrator rights, and that person may change
the installation folder. Updates arrive the same way; only the version under
Apps stays at the one first installed. The task downloads only from GitHub over
HTTPS and installs a file only when it matches checksums.txt.
Uninstalling¶
Apps, ArrowLoop, Uninstall asks for an administrator and removes the program,
its shortcuts and its entry, the scheduled task and %ProgramData%\ArrowLoop
with the switch and the log. The webview cache of the person uninstalling goes
as well. Every user's settings and jobs in %APPDATA%\ArrowLoop stay, so a
later install picks them up.
Trying it locally¶
A build from source is not a release and never updates itself. Only a build of a
version tag, stamped by scripts/desktop.mjs as the release workflow makes it,
does. Built with -tags updatetest, the app reads ARROWLOOP_UPDATE_API, a
stand-in for https://api.github.com, and ARROWLOOP_UPDATE_DELAY, the wait
before the first check (such as 15s), so the whole path can be tried locally.
node scripts/desktop.mjs --installer --updatetest builds an installer for
such a build. It installs as ArrowLoop Test, with a task and a folder under
ProgramData of that name, beside a real installation. Its task starts without
your environment and reads the stand-in's address from updatetest-api in
%ProgramData%\ArrowLoop Test. Release builds leave the tag out, so nothing on
the computer can change where an update comes from.
The builds are not signed
Windows shows its blue warning on first start (More info, then Run anyway),
and macOS needs a right-click and Open the first time. That is a deliberate
trade for now: a certificate is a recurring cost, and it is worth paying
once there are users to pay it for. For the same reason an update is
checked only against the release's checksums.txt, which proves the file
is the one the release published, not who built it.
On Android¶
The Android app runs the engine on the phone itself, so the phone's own files
can take part in a job. Every release carries the arm64 APK,
arrowloop-android-arm64.apk,
which is what phones run; an x86_64 build for the Android emulator is in the
artifacts of each CI run.
The APK is signed with the project's own key, so a later release installs over an earlier one. An APK from an ordinary CI run carries the public debug key instead, and has to be uninstalled before a release can be installed over it.
What the app keeps on the phone and what it sends where is in the privacy policy.
As a single binary¶
The result needs nothing else installed. It carries the interface, the engine and every storage backend it supports.
Syncing between two places over the internet¶
ArrowLoop connects to the other side itself, over SFTP, SMB, WebDAV or a cloud's own interface. It has no relay and no account of its own, so a laptop away from home reaches the server at home only if that server can be reached. Opening its SSH port to the internet is the way not to do that.
Put both machines in one private network instead:
- Tailscale is the quickest. Install it on both machines and they reach each other by name, from anywhere, with no port forwarded. It needs a Tailscale account, its coordination servers introduce the machines, and its relays carry the traffic when no direct path exists. That is the same trade Syncthing and Resilio make, taken once for every program rather than per program. Headscale runs the coordination on your own server.
- WireGuard on its own needs nobody else's server, but one UDP port forwarded on the router at home.
Then add the server in ArrowLoop as usual, under Targets, Add a server or
share, with its name or address inside that network as the host: with
Tailscale, the machine name it shows (server or
server.your-tailnet.ts.net); with WireGuard, the address the tunnel gives it.
A job between the laptop and that target then runs the same at home and away.
While the laptop has no connection, its scheduled runs fail and say so, and
the next one that gets through catches up.