Once you have uploaded a site twice, you stop wanting to drag folders. What you want is: make the server look like this local folder, upload only what changed, and tell me what you are about to do before you do it.
That is folder synchronisation, and the details of how it decides what “changed” means are worth knowing — because when a sync goes wrong it goes wrong quietly.
How comparison actually works
A sync compares two directory trees and classifies each file: only local, only remote, newer on one side, or identical. To do that it needs, for every file, a name, a size and a modification time.
Size is reliable. Timestamps are not, and this is the part that causes trouble.
A classic FTP LIST response is formatted for humans, with a resolution of one minute — and for files older than six months it typically shows a year instead of a time, so the resolution degrades to a day. Your local filesystem records modification times with sub-second precision. Comparing them directly produces false differences on every single run.
A correct implementation compares with a tolerance, and falls back to size when the times are within it. Upstream does exactly this: mtimes are compared with a tolerance to absorb the minute-level granularity of LIST, and files that tie on time are then compared on size.
Servers that support the MLSD command (RFC 3659) return a proper machine-readable timestamp, which removes the guesswork — another reason to prefer a modern server, and the same mechanism behind hidden files appearing correctly. SFTP has structured metadata throughout and does not suffer from any of this.
Timezones, the other timestamp trap
FTP servers report times in their own timezone, and RFC 959 never said which one that should be. A server in UTC and a Mac in CEST will disagree by two hours, which is more than any sane tolerance — so a naive sync decides that every file differs, and re-uploads your entire site.
If your first sync wants to transfer everything, this is the usual reason. MLSD timestamps are defined as UTC, which is the clean fix.
Mirror mode deletes things
The two modes are very different in consequence:
Upload/update copies new and changed files to the server and leaves everything else alone. It is safe and it is what you want most of the time. Its only downside is that files you deleted locally stay on the server forever.
Mirror makes the destination identical to the source, which means deleting anything on the server that is not present locally. This is what you want for a clean deploy — and it is also how people delete a uploads/ directory full of user content that only ever existed on the server.
Before any mirror, ask one question: does the server hold anything that does not exist locally? User uploads, generated caches, log files, a .well-known directory placed there by your certificate tooling. If yes, either exclude it or do not mirror.
This is what a dry run is for. Upstream’s sync shows a full preview of every planned action — upload, download, delete, skip — before anything happens. Read the delete list. It costs ten seconds and it is the difference between a deploy and an incident.
Excluding files with .syncignore
Exclusions belong in a file next to your project rather than in a settings dialog you will forget. Upstream reads a .syncignore from the local root, using a gitignore-style syntax:
# hidden directories (.git, .next, .cache, .idea …)
.*/
# OS junk
.DS_Store
Thumbs.db
# dependencies — never deployed
node_modules/
__pycache__/
# temporary files
*.swp
*~
The supported syntax is a deliberate subset: * matches within one path segment, ** matches any depth, ? matches one character, a trailing / restricts a rule to directories, and a leading / anchors it to the root. A bare name matches at any depth, exactly as gitignore does. There is no ! negation — it was left out on purpose, because negation is where ignore files become hard to predict.
Two behaviours worth knowing: the rules are applied to both sides, so an excluded path is neither uploaded nor deleted by a mirror. And if you provide a .syncignore, it replaces the defaults rather than adding to them — so if you write your own, remember to re-add .DS_Store and friends.
Note also that the defaults exclude hidden directories but not hidden files: .htaccess and .env are real content on a web server, and silently skipping them would be worse than the alternative.
Conflicts
A conflict is when both sides changed since the last time they agreed. There is no automatic answer that is right in general — the correct behaviour is to stop and ask, per file, and to make it obvious which version is which. Any tool that resolves conflicts silently is making a decision you would probably not have made.
Step by step
- Connect and navigate both panes to the folders you want to align — local build output on one side, web root on the other.
- Add a
.syncignoreto the local root before the first run, covering.git/,node_modules/,.DS_Storeand anything holding credentials. - Choose the direction deliberately — upload/update for a normal deploy, mirror only when the server should be an exact copy.
- Run the dry run and read it, paying particular attention to the delete list.
- Apply, and let the queue work — transfers run concurrently and resume if the connection drops.
- Spot-check the result in the remote pane, using the comparison colours to confirm the two sides now agree.
Questions people ask
Why does sync want to re-upload every file?
Almost always a timestamp problem: either the server reports times in a different timezone, or its LIST resolution is too coarse to compare against your local sub-second times. A server supporting MLSD, or SFTP, removes the ambiguity.
Will sync delete files on my server?
Only in mirror mode, which exists precisely to make the destination identical to the source. Upload/update mode never deletes. Always read the dry-run preview before applying a mirror.
Does sync see hidden files like .htaccess?
It depends on the protocol. Over SFTP, always. Over FTP it depends on the client asking the server for them properly — see hidden files over FTP. If it does not, dotfiles are silently never compared.
Can I sync only part of a folder?
Yes — sync the specific subfolder, or use a .syncignore to exclude what you do not want. Exclusions apply to both sides.
Is FTP sync as reliable as rsync?
No, and it is worth being honest about that. rsync compares file contents with rolling checksums and transfers only the changed blocks. FTP has no equivalent, so any FTP-based sync compares metadata and transfers whole files. If you have SSH access and can run rsync, it is technically superior; a GUI sync wins on convenience and on servers where you only have file transfer access.
What happens if the connection drops mid-sync?
Completed transfers stay done. Upstream’s queue retries with exponential backoff and resumes interrupted files from where they stopped, so re-running the sync afterwards only handles what genuinely remains.