FileSharer Development Notes 9 — Restoring the Share List After a Restart
FileSharer Development Notes · 09 / 10

If share links disappear every time the program restarts, it is hard to rely on addresses you have already sent. FileSharer therefore saves its in-memory share entries to shares_db.txt and reads them back at startup. Even for a small tool that stores everything in one file, ‘it can write the data’ and ‘it can recover after an interruption’ are different requirements.

The sample results below use two simple strings, rather than the actual share database, to illustrate the save and backup path.
The final file is not overwritten directly
WriteFileAtomic first writes the new content to a .tmp file alongside the destination. If the destination already exists, it replaces that file with File.Replace, preserving the previous file as .bak. The original function’s preferred path is as follows.
File.WriteAllLines(tmp, lines, Encoding.UTF8);
if (File.Exists(path))
{
try { File.Replace(tmp, path, bak); return; }
catch { }
}
This is followed by a fallback path that, if replacement fails, moves the existing file to the backup name and then moves the temporary file to the final location. Microsoft’s File.Replace documentation describes destination replacement and backup creation, along with restrictions involving different volumes.
Having Atomic in the method name does not make every execution path, including power-loss scenarios, a single indivisible operation. The fallback path, in particular, consists of multiple moves and has intermediate states. This check did not establish when the writes become durable on the file system.
What remains after two writes?
The sample first saves generation-one in a new folder, then saves generation-two to the same path. The file contents after the second write are shown below.
| File checked | Actual contents |
|---|---|
| Final file | generation-two |
| .bak file | generation-one |
In the sample results, a normal second save preserved the previous generation. This verifies the success path only; it does not establish recovery behavior after power loss, permission errors, or insufficient disk space.
‘Empty’ and ‘missing’ are different recovery conditions
LoadStoreFromDisk returns immediately if the primary file does not exist. If the file exists but is empty and the backup has content, it reads the backup. It would therefore be inaccurate to describe the loader as automatically recovering even when only the .bak file remains.
Some values in each row are also converted with bool.Parse, long.Parse, and DateTime.Parse, while exceptions are caught by an outer handler covering a broad block of code. It is worth checking how many of the remaining valid rows are read if a row in the middle is malformed. Per-row validation and error reporting could reduce the chance that a single damaged entry obscures the recovery of the whole store.
The current loader adds only reloaded entries for which IsActive() is true to the in-memory dictionary. A row can therefore remain on disk while being excluded from the restored list because of revocation, expiration, or a download-count limit.
Save results should be reported to the caller
The higher-level SaveStoreToDisk catches exceptions without returning a separate failure result. This means a new share can be visible in memory even though saving it to disk has failed. If the problem becomes apparent only after a restart, identifying the cause is difficult.
One improvement would be to return the save result to the caller and show the user a status such as ‘available for the current session only.’ Other points to review are whether concurrent saves can safely share a fixed .tmp name and whether the order of captured snapshots can be reversed.
Recovery files retain secrets too
The saved share rows contain not only file paths but also PIN and share-encryption-key fields. Backups can retain the same kinds of information. Tunnel authentication values, by contrast, have a separate code path that uses Windows DPAPI. Combining these two storage paths into a claim that ‘all secrets are stored encrypted’ would be incorrect.
DPAPI’s CurrentUser scope binds protection to the current user account. Its meaning is described in Microsoft’s DataProtectionScope documentation. This must not be interpreted as a guarantee of automatic recovery on another PC or under another account.
Even a small settings store needs storage and recovery to be designed together. The next step suggested by this code is to go beyond checking a successful save and treat a missing primary file, an empty file, a damaged row, and a backup containing secrets as separate states.
FileSharer.cs:2661–2694 SaveStoreToDisk · FileSharer.cs:2704–2723 WriteFileAtomic · FileSharer.cs:2725–2794 LoadStoreFromDisk · FileSharer.cs:TailscaleTunnel.SaveAuthKey / LoadAuthKey
댓글
댓글 쓰기