FileSharer Development Notes 2 — Turning 'Download Only Once' into Code

한국어로 읽기 · Part 1: The file-sharing server

FileSharer Development Notes · 02 / 10

Three developers use a key, an hourglass and a ticket to explain PIN, expiry and download limits.
AI illustration · Three developers use a key, an hourglass and a ticket to explain PIN, expiry and download limits.

“Please make this link usable only once.” It sounds like a simple request. But if the recipient loses their connection after downloading 70% of the file, should a retry count as another use? What about refreshing the page? Before implementing a download limit, we need to define what counts as one download.

The original Korean-language PIN page generated by BuildPinInputHtml. No real PIN or user information was entered. Only the page rendering was checked; this is not a validation of the authentication system as a whole.
The original Korean-language PIN page generated by BuildPinInputHtml. No real PIN or user information was entered. Only the page rendering was checked; this is not a validation of the authentication system as a whole.

FileSharer stores an expiration time, a maximum download count, the current count, and a revocation flag for each shared item. For this article, I examined the actual conditions and ran the original function to check its basic state decisions.

Three lines that determine whether a share is active

The core of ShareItem.IsActive() is shown below. These conditions come from the original method.

if (IsRevoked) return false;
if (ExpiresAt.HasValue && DateTime.Now > ExpiresAt.Value) return false;
if (MaxDownloads.HasValue && DownloadCount >= MaxDownloads.Value) return false;
return true;

The PIN is not part of this state check. Whether the share is valid and whether the requester supplied the correct password are separate checks. Keeping them distinct makes it easier to catch combinations such as “Can someone with the correct PIN still download an expired file?”

Testing five states with the actual function

Instead of using live share data, I created a new ShareItem and changed its values. For the expiration test, I used a time one day in the past to avoid ambiguity at the boundary.

Input state IsActive return value
No expiration or download limit; not revoked true
IsRevoked is true false
Expiration time is one day in the past false
Maximum 1 download; current count 1 false
Maximum 1 download; current count 0 true

These results cover five cases for the state function. They do not test two people making requests simultaneously or exercise every HTTP route. The current implementation also reads DateTime.Now directly, so making exact-boundary tests repeatable could require a change such as passing in a clock.

Blocking the landing page is not enough

The usual flow is to open the share page and click the download button. From the server's perspective, however, the page request and the file request are independent. It cannot assume that the user has passed through the first screen.

In the current source, the share page route checks revocation, expiration, the download limit, and the PIN. The full file and ZIP download route also has related checks. The branch for requesting individual files within a share checks the PIN and path, but I did not find code in that branch that repeats the same expiration, revocation, and count checks. It would therefore be inaccurate to say that the same restrictions already apply to every download route.

A clearer design would require every request type to pass through a common authorization function first. This work was limited to writing the articles and reviewing the source; that improvement has not been applied to the actual product.

Resuming requires evidence that this is the same download

The full download branch treats a request with a Range header as a resume request and bypasses some download-limit checks and count increments. The intent is understandable: a dropped connection should not consume another allowed download every time. But a Range header does not, by itself, establish the identity of a previous download.

Strict download limits would require a design that issues a download session and records which share, file version, and permitted scope that session is tied to. Deciding whether to allow a new download and reserving a count should also happen in the same critical section to reduce gaps between concurrent requests.

The current count is also incremented during preparation for the full file or ZIP transfer, rather than after the transfer completes. The displayed count should therefore not be read as “the number of times a recipient has finished saving the file.” Another possible improvement is to align the product wording with the implementation, using a label such as “downloads started.”

The boundary between a PIN page and secret handling

The page shown above was produced by the original HTML generation function. It includes an input form and error messages, but the appearance of the page does not tell us how securely secrets are handled. The current approach passes the PIN in the URL query string and also records its value in the share storage file. Separating authentication information from URLs and defining a storage policy remain distinct tasks.

This article offers three practical distinctions: define whether a share is valid, whether a request is authorized, and when its usage count becomes final. Only then can “download once” match what the server actually guarantees.

Source references
FileSharer.cs:43–49 ShareItem.IsActive · FileSharer.cs:4740–4804 Share page checks · FileSharer.cs:4807–4855 Requests for individual files within a share · FileSharer.cs:4899–4939 Full download limits · FileSharer.cs:5406–5445 BuildPinInputHtml
The original project files were not modified. Proposed improvements are distinguished from implemented behavior.

댓글