File Management

Fast alle Webanwendungen verwenden vom Benutzer hochgeladene Dateien. Von Dokumenten, die mit einigen Modellen in einer Datenbank verknüpft werden müssen, bis hin zu Bildern als Avatare der Nutzer. Je mehr Nutzer Ihr Dienst hat, desto mehr Dateien sammeln sich auf Ihrem Server an. Wenn Sie die Dateiverwaltung vernachlässigen, werden Sie bald ein ernstes Platzproblem haben.

Wo werden die Dateien der Nutzer gespeichert?

Es gibt mehrere Möglichkeiten, vom Benutzer hochgeladene Dateien zu speichern: in einem Dateisystem oder in einer Datenbank. Wie bei allem, haben beide Lösungen ihre Vor- und Nachteile.

Wenn Ihre Anwendung nur eine geringe Anzahl eher kleiner Dateien enthält, die eng mit in einer Datenbank gespeicherten Objekten gekoppelt sein müssen, oder wenn Sie eine sicherere Methode zur Speicherung sensibler Dateien benötigen, sollten Sie Ihre Datenbank dafür verwenden. Allerdings wird diese Lösung die Größe Ihrer Datenbanksicherungen erheblich erhöhen und möglicherweise erfordern, dass Sie einige Dateien in Blobs umwandeln.

Die Speicherung von Dateien in einem Dateisystem ist sinnvoller, wenn Sie mit großen Dateien oder einer großen Anzahl von Benutzern zu tun haben. Die Vergrößerung eines Dateisystems ist viel billiger als der Kauf von mehr Datenbankplatz. Ein weiterer Vorteil dieses Ansatzes ist die einfache Migration. Wenn Sie irgendwann einmal von Benutzern hochgeladene Dateien auf einen anderen Server oder S3 migrieren müssen, ist dies viel einfacher, als sie aus Ihrer Datenbank zu extrahieren und dann zu migrieren.

Wie verwaltet man Dateien in einem Dateisystem?

Bei der Implementierung von Dateiverwaltungsfunktionen für Ihre Serveranwendung müssen Sie einige Dinge beachten:

  • alle gespeicherten Dateien müssen leicht identifizierbar sein
  • Sie müssen ungenutzte Dateien löschen
  • Sie müssen den Zugriff auf andere Dateien beschränken

Identifizierung von Dateien

Da Sie wahrscheinlich Daten aus einer Datenbank mit bestimmten Dateien im Dateisystem verknüpfen müssen (z. B. einen Benutzer mit seinem Avatar) und Sie einige Daten über die Datei speichern müssen, um die Verwendung zu erleichtern, ist es üblich, ein Dateimodell zu erstellen und es in Ihrer Datenbank zu speichern. Es könnte wie folgt aussehen:

case class File(  id: UUID,  contentType: String,  path: String,  filename: String) 

Wir werden in der Lage sein, den Benutzer mit seinem Avatar zu verbinden, indem wir die Datei-ID als Fremdschlüssel speichern. In der Variablen path wird ein relativer Pfad zum Bild gespeichert, contentType steht für den MIMO-Typ der Datei und filename für den ursprünglichen Namen der Datei.

Stellen wir uns ein Szenario vor, in dem zwei Benutzer versuchen, ihr Avatarbild „image.jpg“ hochzuladen. Was würde passieren, wenn wir diese beiden Dateien im selben Verzeichnis speichern? Die eine würde die andere überschreiben. Um dies zu verhindern, werden wir für jede hochgeladene Datei ein eigenes Verzeichnis erstellen. Um sicherzustellen, dass es eindeutig ist, verwenden wir den aktuellen Zeitstempel als Namen.

Kommen wir zurück zum Modell File . Unsere Anwendung wird ein bestimmtes Verzeichnis zum Speichern aller Dateien haben (base_file_dir). Da wir wissen, dass jede hochgeladene Datei in einem separaten Unterverzeichnis gespeichert wird, können wir path auf „timestamp/file_name“ setzen. Wenn wir nun auf diese Datei zugreifen wollen, können wir ihren teilweisen Speicherort aus unserer Datenbank abrufen und ihn mit dem Basisverzeichnis verknüpfen, um einen absoluten Pfad zu erhalten.

 

Wichtig!

Beim Abrufen von Dateinamen aus Daten, die von einem Benutzer gesendet wurden, müssen Sie darauf achten, diese zu bereinigen. Sie sollten nur die Erweiterung und den letzten Teil des Pfades nehmen und ihn von „..“ bereinigen. Dadurch wird verhindert, dass der Benutzer Dateien außerhalb eines bestimmten Verzeichnisses speichert.

Datei bereinigen

Nehmen wir an, unser Benutzer hat ein Formular mit einer angehängten Datei gesendet, aber das Formular ist nicht korrekt und unser Server gibt einen Fehler zurück. Was soll mit der Datei geschehen? Sie können die Datei vor oder nach der Validierung des Formulars speichern. Wenn Sie sie danach speichern, muss der Benutzer sie nach der Korrektur des Formulars erneut senden. Wenn Sie die Datei vor der Validierung speichern, muss der Benutzer sie nicht erneut senden, aber wenn er das korrigierte Formular nicht sendet, haben Sie eine unbenutzte Datei. Um dies zu verhindern, sollten wir eine Form der temporären Dateispeicherung implementieren.

Die temporäre Datei ist ein separates Verzeichnis innerhalb von base_file_dir, nennen wir es tmp. Alle Dateien, die an den Server gesendet werden, werden zunächst in tmp gespeichert (in einem separaten Verzeichnis mit einem Zeitstempel als Namen) und es wird noch kein File Objekt erstellt. Wenn der Server feststellt, dass unser Formular gültig ist, wird die gespeicherte Datei von tmp nach base_file_dir verschoben, und dasFile Objekt wird mit dem Pfad an seinem neuen Speicherort erstellt.

Im Moment gibt es keinen Unterschied zwischen temporärem Speicher und Basisspeicher. Um tmp temporär zu machen, müssen wir eine Methode erstellen, die in regelmäßigen Abständen ausgeführt wird und einige Dateien aus tmp. löscht. Wir können nicht alle Dateien löschen. Einige von ihnen können mit gültigen Formularen verbunden sein, die gerade überprüft werden, oder mit Formularen, die der Benutzer gerade korrigiert. Um dies zu verhindern, können wir Dateien löschen, die älter als eine gewisse Zeit sind – sagen wir 30m. Dazu erstellen wir eine Methode wie die folgende und führen sie mit Play ScheduleModule aus.

def clearTempFiles(): Unit = {    val currentTimestamp = LocalDateTime.now().minusSeconds(1800).atZone(ZoneId.systemDefault()).toInstant.toEpochMilli    val tmpDirectory = new java.io.File(s"${base_file_dir}/tmp") // Base tmp file directory    if (tmpDirectory.exists()) { // if tmp directory exists        tmpDirectory.list().filter(_.toLong < currentTimestamp).map { subDirectory => // filter all files in tmp dir, that have timestamp lower then current time - 30min            val fullDirPath = s"${tmpDirectory.toPath}/${subDirectory}"            try {                val dir = new Directory(new io.File(fullDirPath))                if (!dir.deleteRecursively()) {                logger.error(s"Unable to delete directory: ${fullDirPath}")                }            } catch {                case e: Throwable =>                logger.error("Delete file error!")            }        }    }} 

 

Servieren von Dateien

Nach dem Speichern einer Datei ist es an der Zeit, sie den Benutzern zur Verfügung zu stellen. Eine der sichersten Methoden für den Zugriff auf die Datei ist die Verwendung ihrer ID. Der Server sucht in der Datenbank nach einem Datei-Objekt mit einer bestimmten ID und ruft dann eine Datei aus base_file_dir anhand des in der Datenbank gespeicherten relativen Pfads ab. Da die Benutzer keinen Zugriff auf irgendeine Form des Dateipfads haben, ist es für sie unmöglich, auf eine andere Datei zuzugreifen.