Bestanden tussen werkplekken synchroniseren
Hoe ik de client ontwierp en bouwde die een lokale map met andere werkplekken verbindt.
Een lokale map op meerdere werkplekken bijhouden
Voor de module Systems & Security bij Avans moesten we een applicatie voor bestandssynchronisatie ontwerpen en bouwen. Zoals bij OneDrive moest een gebruiker bestanden in een lokale map kunnen bewerken, waarna die wijzigingen ook op andere werkplekken terechtkomen. Op iedere werkplek was daarvoor een client nodig: een programma dat de map bijhoudt en bestanden uitwisselt met een centrale server.
Met een team van drie studenten bouwden we een Python-client en een server in C# met .NET. Ik ontwierp en bouwde de client, waaraan ook teamgenoten bijdroegen. De opdracht vroeg om een programmeertaal waarmee we nog niet vertrouwd waren. Omdat ik vooral PHP kende en al kort met Python had kennisgemaakt, koos ik Python voor de client. WebSockets kozen we als team voor de communicatie; de berichtverwerking werkten we zonder framework uit.
De server moest meerdere clients kunnen bedienen, ook op verschillende besturingssystemen. Client en server moesten bovendien op afzonderlijke computers draaien. Naast de uitwisseling van grote bestanden stelde de opdracht eisen aan het voorkomen van dubbele, corrupte en onvolledige bestanden. Die eisen raken direct aan mijn clientwerk: een download verandert dezelfde map die de client bewaakt. Daardoor moest ik zowel wijzigingen van de gebruiker als de gevolgen van de eigen bestandsoverdracht verwerken.
Voordat we bouwden, beschreven we de gebruikersbehoeften en eisen. Per functionele eis legden we een prioriteit en een voorgenomen testmethode vast. Voor bestandssynchronisatie was dat bijvoorbeeld een integratietest, waarin client en server samen worden beproefd. Zo koppelden we het gewenste gedrag vooraf aan de manier waarop we het wilden controleren.
Ook de afspraken over berichten werkten we eerst uit. Sequentiediagrammen lieten de volgorde van aanmelden, synchroniseren, uploaden en downloaden zien; de protocolspecificatie beschreef de berichttypen, velden en voorbeelden. Voor een extra punt moest onze client kunnen samenwerken met de server van een andere groep. We stemden het protocol daarom met die groep af. Die afstemming gaf ons gedeelde afspraken voor de bouw, maar toont op zichzelf geen geslaagde compatibiliteitsproef aan.
Probeer de browserdemo om de uitwisseling tussen twee werkplekken te volgen. Deze reconstructie is voor het portfolio gemaakt.
De client houdt de synchronisatiemap op een werkplek bij. De centrale server ontvangt bestanden en geeft wijzigingen door aan de andere clients. Mijn ontwerp en implementatie lagen aan de clientkant.
Eerst een wijziging melden, daarna het bestand versturen
Als een lokaal bestand verandert, meldt de client dat aan de server. De server beoordeelt de versie en vraagt zo nodig om een upload. Na de overdracht geeft hij de wijziging door aan alle clients die zich voor meldingen hebben aangemeld, inclusief de bronclient. Iedere ontvanger bepaalt vervolgens of zijn lokale kopie moet worden bijgewerkt.
We hielden meldingen en bestandsinhoud daarbij gescheiden. Een melding bevat onder meer de bestandsnaam, grootte en inhoudshash: een berekende herkenningswaarde van de inhoud. Daarmee kan de ontvanger eerst beoordelen of een overdracht nodig is. Meldingen en opdrachten gaan over een blijvende WebSocketverbinding. Pas voor de upload of download zelf opent de client een aparte verbinding, zoals in de procesflow uit onze presentatie hieronder.
In de implementatie: In de code filtert de Python-client lokale events voordat hij ze meldt. De server vergelijkt eerst de hash en daarna de timestamp; het uploadverzoek heet REQUEST_UPLOAD.
Mapbewaking verbinden met netwerkverwerking
In de client komen lokale gebeurtenissen en berichten van de server samen. Watchdog bewaakt de map, terwijl asyncio de netwerktaken aanstuurt. Als een van die taken op netwerkverkeer wacht, kunnen andere taken verdergaan. Beide onderdelen draaien binnen dezelfde Python-applicatie; de .NET-server staat daarbuiten.
De overgang via wachtrijen uitwerken
Bij een wijziging roept Watchdog een callback aan die op de bestandsgebeurtenis reageert. Die callbacks volgen het Observer-principe en draaien in een eigen thread. De netwerktaken werken onder de eventloop van asyncio. Om een bestandsgebeurtenis daar te verwerken, moet de client haar eerst vanuit de Watchdog-thread overdragen.
Voor die overgang stelde ik het producer–consumer-patroon voor. Het ene onderdeel levert werk aan via een wachtrij, het andere leest dat werk eruit en verwerkt het. In de client werkte ik dit uit met een FileEvent, waarin de eventhandler de bestandsgebeurtenis vastlegt. Via asyncio.run_coroutine_threadsafe(...) plant hij het plaatsen van dat object in watcher_queue op de eventloop. Een afzonderlijke taak leest de queue en handelt de gebeurtenis af. Als daaruit een melding aan de server volgt, komt die in websocket_queue voor de verzendtaak.
In EventHandler.handle_file_event() in watcher.py staat de overdracht naar de eventloop:
asyncio.run_coroutine_threadsafe(self.queue.put(file_event), self.loop)
De wachtrijen geven iedere stap een eigen verantwoordelijkheid: de watcher levert een gebeurtenis, de client verwerkt haar en de verzendtaak verstuurt het bericht. Zo vindt de netwerkoverdracht buiten de Watchdog-callback plaats. Bestandscontroles en hashing blijven wel synchroon in die callback staan; de watcher-thread kan daar dus nog op wachten.
Berichten die van de server komen, volgen een ander pad. De ontvangende taak, websocket_consumer, beoordeelt na een melding over een nieuw of gewijzigd bestand of een download nodig is. Een uploadopdracht start rechtstreeks een upload. Voor beide opent de taak een aparte verbinding en wacht zij op de overdracht. De verzendtaak, websocket_producer, kan intussen lokale meldingen uit haar eigen wachtrij versturen. Inkomende serverberichten gaan dus niet vanzelf via die queue weer terug.
Dat onderscheid bepaalt ook hoeveel werk tegelijk gebeurt. Zolang de ontvangende taak op een overdracht wacht, handelt zij geen volgend serverbericht af. Tijdens een synchronisatieronde worden bestanden eveneens achter elkaar overgedragen. De queues scheiden de verantwoordelijkheden, maar maken niet iedere bewerking gelijktijdig.
Vaststellen of de lokale kopie afwijkt
Om te bepalen of een download nodig is, heeft de client meer nodig dan alleen een bestandsnaam. Bij het starten vraagt hij met SYNC de bestandsgegevens van de server op. Hij vergelijkt die met de grootte en SHA-256-inhoudshash van het lokale bestand. Voor die lokale vergelijking is geen wijzigingstijd nodig, al moet de client voor de hash wel de bestandsinhoud lezen.
Daarmee kan de client een verschil herkennen. Welke versie moet winnen als twee werkplekken hetzelfde bestand wijzigen, is een afzonderlijke vraag. De server gebruikt in zijn versieafweging ook tijdstippen; de lokale hashvergelijking lost zulke conflicten dus niet in het algemeen op.
In de implementatie: De implementatie vergelijkt SHA-256 en bytegrootte. Zij uploadt ook bestanden die alleen lokaal bestaan. Die uploadstap staat niet in de oorspronkelijke flow.
Voor alleen een overzicht van bestandsnamen bevat het protocol daarnaast LIST. Dat verzoek staat op zichzelf: de Python-client hoeft het niet uit te voeren voordat hij met SYNC begint.
De watcher reageert ook op een download
Zodra een download naar de lokale map schrijft, kan Watchdog daarop reageren, ook als het bestand nog niet volledig binnen is. Ik werkte daarom aan bestandslocks om bewerkingen aan dezelfde bestandsnaam op elkaar af te stemmen. De client gebruikt één asyncio.Lock per naam. Zolang een bewerking die lock vasthoudt, slaat de watcher gebeurtenissen voor dat bestand over. Dat beperkt overlap, maar kan ook een wijziging van de gebruiker overslaan. Er is geen buffer om die gebeurtenis na de overdracht alsnog te verwerken.
Bij het afronden van uploads begrenst de client het wachten op een antwoord. Nadat hij EOF, het einde-van-bestandssignaal, heeft verstuurd, wacht hij maximaal tien seconden op een status. Bij een timeout of ongeldige status probeert hij het nog één keer: maximaal twee pogingen in totaal. Die grens geldt voor de bevestiging na EOF, niet voor de duur van de volledige upload.
Volg een wijziging in de browserdemo
De demo laat zien hoe een wijziging tussen twee werkplekken wordt doorgegeven. Voor deze portfolioreconstructie houdt een relay, het tussenstation voor de uitwisseling, de toestand in het geheugen bij. Er draait geen oorspronkelijke Python-client of .NET-server achter en er wordt geen echte projectmap gesynchroniseerd.
Kies Start omgeving, bewerk een bestand op de bronclient en kies Opslaan en synchroniseren. In de relayterminal kun je de tussenstappen volgen. De bestandsverkenner en mappaden zijn onderdeel van deze reconstructie; de oorspronkelijke watcher bewaakt alleen de bovenste map. De demo toont de gegevensstroom, zonder netwerkverlies of versieconflicten te beproeven.
Interactieve bestandssynchronisatie
Bewerk een lokaal bestand
Watchdog → asyncio.Queue → client task
SSP-relayterminal
- relay@cachyos:~/ssp/Server$
Controleer de lokale kopie
WebSocket consumer → hashcontrole → FileStore
# Shared notes A small sync surface for the team.
Extra client
WebSocket consumer → lokale FileStore
Geen inhoud beschikbaar.
Waar de afhandeling van bestanden nog tekortschiet
Een onderbroken download raakt het doelbestand
De client schrijft een download rechtstreeks naar het doelbestand. EOF geeft aan dat de overdracht is afgerond, maar daarna controleert de implementatie de hash en grootte niet opnieuw. Bij een mislukte overdracht probeert de client het onvolledige bestand te verwijderen. De oude inhoud kan dan al overschreven zijn. Daarmee dekt de implementatie de opdrachteis om onvolledige bestanden te voorkomen nog niet volledig af.
Verwijderingen hebben een eigen bericht
Een verwijderd bestand kan de client niet meer lezen om de inhoudshash en grootte vast te stellen. Het lokale verwijder-event krijgt daarom een lege hash en grootte nul. Aan de ontvangende kant verwerkt de client een verwijdering via een deleted-notification. De procesflow noemt deze route DELETE; in de Python-code loopt zij via een melding, terwijl opdrachten de uploads en downloads starten.
Wat de tests aantonen
De Python-client bevat unit-tests voor hulpfuncties en mapbewaking. Met mocks, vervangingen van afhankelijkheden, controleren ze afzonderlijke stukken logica. Bij de broncodecontrole op 7 september 2026 slaagden veertien tests. De twee watcher-tests raken alleen negatieve paden en toetsen de werkelijke overdracht naar de asyncio-queue niet. Deze controle toont ook geen volledige client-serverproef, overdracht van meerdere gigabytes of werking op alle gevraagde besturingssystemen aan.
Mijn bijdrage: van bestandsgebeurtenis naar uitwisseling
Binnen ons teamsysteem ontwierp en bouwde ik de Python-client die de lokale map met de server verbindt. Mijn queuevoorstel gaf de overdracht tussen mapbewaking en netwerkverwerking een concrete plaats in de implementatie. Daarnaast werkte ik aan uploads, downloads en locks: de bewerkingen waarmee die berichten gevolgen krijgen voor de lokale bestanden.
De client laat daarmee zien hoe ik gebeurtenissen uit verschillende onderdelen samenbracht en de verwerking verdeelde. De afhandeling van de bestanden zelf bleef daarbij een afzonderlijke verantwoordelijkheid. Vooral de rechtstreeks overschreven downloads laten de grens van het resultaat zien: de scheiding tussen taken alleen voldeed nog niet aan alle eisen rond onvolledige bestanden.