Steffen Peters: Datei aus dataTransfer.items in Häppchen lesen?

Hallo,

ich habe ein kleines Upload-Tool gebaut, das aber bisher nur Dateien hochladen konnte. Jetzt möchte ich das Tool erweitern, sodass auch komplette Ordner hochgeladen werden können.

Da ich die Struktur beibehalten möchte/muss, bin ich anstatt auf dataTransfer.files auf dataTransfer.items gewechselt. Hier bekomme ich auch den Pfad und kann mir rekursiv alle Dateien erarbeiten.

Nun möchte ich aber weiterhin, dass die Dateien in kleine Häppchen zerlegt hochgeladen werden. Leider funktioniert hier das von mir verwendete file.slice(start, ende) nicht mehr.

Wahrscheinlich bin ich einfach nur zu blöd, aber ich finde aktuell keine Alternative dazu. Hat jemand von euch vielleicht eine Idee?

LG Steffen

  1. Hallo Steffen,

    mit dem Item direkt fängst Du auch nicht viel an.

    Du kannst webkitGetAsEntry aufrufen, um ein FileSystemFileEntry oder FileSystemDirectoryEntry Objekt zu erhalten. Sagt MDN - mein Chrome kennt die beiden Typen nicht. Aber das erhaltene Objekt hat die Eigenschaften isDirectory und isFile, die kannst man abfragen.

    Von einem FileSystemFileEntry kannst Du mit der getFile() Methode wieder ein File-Objekt bekommen, das Du nach Lust und Laune slicen kannst.

    Einen FileSystemDirectoryEntry musst Du erstmal einlesen, dafür gibt's dort createReader() und der Reader bietet wiederum ein readEntries() an und die Methode bekommt einen Callback, der wiederum FileSystem-Einträge bekommt (und bei großen Directorys auch mehrfach aufgerufen werden kann) und wo Du Unterverzeichnisse rekursiv einlesen musst und und und... ächz.

    Jedenfalls bekommst Du darüber einen Haufen FileSystemFileEntry Objekte und von denen mit getFile ein File Objekt und das kannst Du slicen.

    Lektüre, aber die kennst Du sicher. Ich kannte das bisher noch nicht und musste jetzt erstmal schmökern.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      Von einem FileSystemFileEntry kannst Du mit der getFile() Methode wieder ein File-Objekt bekommen, das Du nach Lust und Laune slicen kannst.

      Das werde ich mir in jedem Fall anschauen.

      Einen FileSystemDirectoryEntry musst Du erstmal einlesen, dafür gibt's dort createReader() und der Reader bietet wiederum ein readEntries() an und die Methode bekommt einen Callback, der wiederum FileSystem-Einträge bekommt (und bei großen Directorys auch mehrfach aufgerufen werden kann) und wo Du Unterverzeichnisse rekursiv einlesen musst und und und... ächz.

      So kompliziert fand ich das nicht, isFile nutze ich dabei und createReader auch.

      function iterateFilesAndDirs(item,path) {
      	path = path || "";
      	if (item.isFile) {		// ist eine Datei
      		allfiles.push(item);
      		previewFile(item,path);
      	} else if (item.isDirectory) {
      		// hole Verzeichnis-Inhalt
      		var dirReader = item.createReader();
      		dirReader.readEntries(function(entries) {
      			for (var i=0; i<entries.length; i++) {
      				iterateFilesAndDirs(entries[i], path + item.name + "/");
      			}
      		});
      	}
      }
      
      

      LG Steffen

      1. Hallo Rolf,

        laut meinem Debugger ist die Methode getFile() nicht vorhanden.

        
        var dt = e.dataTransfer;
        var items = dt.items;
        for (var i=0; i<items.length; i++) {
        	item = items[i].webkitGetAsEntry();
        	iterateFilesAndDirs(item);
        }
        
        function iterateFilesAndDirs(item,path) {
        	path = path || "";
        	if (item.isFile) {		// ist eine Datei
        		allfiles.push(item);
        		previewFile(item,path);
        	} else if (item.isDirectory) {
        		// hole Verzeichnis-Inhalt
        		var dirReader = item.createReader();
        		dirReader.readEntries(function(entries) {
        			for (var i=0; i<entries.length; i++) {
        				iterateFilesAndDirs(entries[i], path + item.name + "/");
        			}
        		});
        	}
        }
        

        Beim Upload laufe ich durch alle Einträge von allfiles[]. Hier benötige ich dann das File-Objekt, um die Größe der Datei zu ermitteln und die Datei scheibchenweise zu lesen.

        Laut Debugger ist jeder Eintrag sauber vom Typ FileSystemFileEntry. Als Methode finde ich nur <prototype>. Darunter gibt es zwar eine Methode file(), aber wenn ich diese aufrufe, erhalte ich die Fehlermeldung: Uncaught TypeError: 'file' called on an object that does not implement interface FileSystemFileEntry.

        LG Steffen

        1. Hallo Steffen,

          das ist alles etwas durcheinander. Im Browser, meine ich.

          Um die Sache nicht zu leicht zu machen, kannst Du von einen DataTransferItem eine Datei als File (getAsFile), als FileHandle (getAsFileHandle) und als FileEntry (webkitGetAsEntry) bekommen. Alle drei gehören zu unterschiedlichen Schnittstellenfamilien.

          Erstens gibt es das File API, wohin das File Objekt gehört. Das kennst Du, das kannst Du slicen, streamen und arrayBuffern.

          Zweitens gibt es das File System Access API, das Dir mit window.showOpenFilePicker() oder windowDirectoryPicker Handles auf Files und Directorys liefert.

          Dort gibt es das FileSystemFileHandle mit kind, type und der getFile() Methode, die zum Handle ein File-Objekt liefert, aber nicht direkt, sondern als Promise, das Du awaiten musst oder auf dem Du mit .then einen Callback registrieren musst.

          Drittens gibt es das File and Directory Entries API. Das simuliert angeblich nur ein lokales Filesystem, aber ein genauerer Blick zeigt, dass das mitnichten eine Simulation ist, sondern ein Blickfenster in einen realen Directory-Teilbaum deines Dateisystems. Zumindest was das Auflisten von Dateien angeht.

          In diesem API gibt es die FileSystemEntry Schnittstelle mit ihren Ausprägungen für Directory und File.

          Ein FileSystemDirectoryEntry** Objekt hat ebenfalls eine getFile() Methode. Die tut aber etwas anderes als ihr Brüderchen im FileSystemFileHandle! Das FileSystemDirectoryEntry zeigt auf einen Directory-Teilbaum, und mit getFile lokalisierst Du darin eine Datei. Über einen Callback bekommst Du einen FileSystemFileEntry dafür.

          Dieser FileSystemFileEntry wiederum hat eine file() Methode, die Dir über einen Callback ein File-Objekt bereitstellt, um auf die Datei zuzugreifen.

          Wenn man davon irgendwas durcheinanderbringt, passiert nichts sinnvolles mehr.

          Rolf

          --
          sumpsi - posui - obstruxi
          1. Hallo Rolf,

            ich teste aktuell mit Firefox unter Windows 11.

            LG Steffen

            1. Hallo Steffen,

              ich habe oben nochmal editiert.

              Was mir auch noch einfällt: Du hast - meine ich - nur im Drop-Event wirklich Zugriff auf die FileSystemFileEntry Objekte. Im Debugger nicht.

              Und eine https-Verbindung ist ebenfalls obligatorisch.

              Für praktischere Tests müsste ich jetzt so eine Seite erstmal selbst programmieren...

              Rolf

              --
              sumpsi - posui - obstruxi
              1. Hallo Rolf,

                done

                Funktioniert in Chrome und Firefox.

                Hauptherausforderung ist, dass die Abläufe asynchron sind und man die Daten in Callbacks bekommt. Wie man das in deinem Kontext sauber zusammenbindet, musst Du selbst schauen.

                Rolf

                --
                sumpsi - posui - obstruxi
                1. Vielen Dank Rolf,

                  damit funktioniert es tatsächlich und ich habe es auch schon in mein Script einbauen können.

                  function iterateFilesAndDirs(item,path) {
                  	path = path || "";
                  	if (item.isFile) {		// ist eine Datei
                  		item.file((file) => {
                  			file.filepath = path + file.name;
                  			allfiles.push(file);
                  			previewFile(file,path);
                  		})
                  	} else if (item.isDirectory) {
                  		// Get folder contents
                  		var dirReader = item.createReader();
                  		dirReader.readEntries(function(entries) {
                  			for (var i=0; i<entries.length; i++) {
                  				iterateFilesAndDirs(entries[i], path + item.name + "/");
                  			}
                  		});
                  	}
                  }
                  

                  Mit asynchronen Abläufen habe ich mich bisher auch noch nicht auseinandergesetzt, habe aber bereits 2 Ansätze finden können:

                  Stackoverflow Link 1 Stackoverflow Link 2

                  LG Steffen

                  1. Hallo Steffen,

                    JavaScript macht alles asynchron, was länger als ein Augenklimpern dauert. Der Grund ist, dass es zusammen mit der Layout-Engine des Browsers in einer Ausführungskette hängt und diese keine Parallelverarbeitung vorsieht.

                    Es gibt im Browser mehrere Ereignisquellen. Timer, Bildschirmanzeige, Maus, Tastatur, und auch Vorgänge, die im Hintergrund ablaufen und fertig werden.

                    Jede Quelle kann ein Ereignis auslösen, und wenn auf dieses Ereignis ein JavaScript-Handler registriert ist, wird er ausgeführt. Nach dieser Ausführung wird die Anzeige neu layoutet.

                    Das muss alles fix gehen, denn solange ein Eventhandler läuft, kann kein anderer laufen. Um Dir und mir die Hölle der Multithreading-Programmierung fernzuhalten. Und deswegen werden Vorgänge, die auf externe Ressourcen zugreifen müssen, geteilt. Du stößt sie in deinem Script an, der Browser führt sie im Hintergrund aus und wenn sie fertig sind, melden sie sich zurück. DAS ist dann aber ein neues Event.

                    Bau mal in deine Funktionen console.log Aufrufe ein, die Dir ausgeben, wann iterateFilesAndDirs beginnt und endet, und wann deine beiden Callbacks beginnen und enden.

                    Man sieht es gelegentlich auch auf dem Bildschirm. Der Inhalt eines Unterverzeichnisses wird von meinem Fiddle nachträglich ins DOM eingebaut. Deswegen kann es zu einem kurzen Flackern kommen, wenn sich ein Teil der Anzeige auf einmal nach unten verschiebt. Bei Dir mag das anders sein, weil Du nur Dateien einsammelst, aber auch hier musst Du aufpassen.

                    Denn sowohl dein .push(file) Aufruf wie auch die rekursiven Aufrufe von iterateFilesAndDirs sind asynchron, d.h. sie sind längst nicht fertig, wenn dein drop-Eventhandler fertig ist. Wenn Du aus dem drop-Eventhandler die iterateFilesAndDirs Funktion aufrufst und sie zurückkehrt, steht in allFiles vermutlich noch gar nichts drin. Das füllt sich erst nach einiger Zeit.

                    Woher weißt Du, dass Du weitermachen kannst?

                    Zunächst solltest Du in einer Variable neben allFiles zählen, wieviele Files Du gefunden hast. Diesen Zähler erhöhst Du im isFile Teil vor dem item.file() Aufruf.

                    Sodann schreibst Du eine Funktion uploadFile. Die rufst Du nicht sofort auf, sondern mit einer kurzen Verzögerung. Wenn Dir nämlich jemand einen Ordner droppt, der NUR Unterverzeichnisse enthält, dann ist nach dem ersten iterateFilesAndDirs Aufruf das allFiles Array noch leer. Bis sich das füllt, müssen die DirectoryReader für die Unterverzeichnisse durch sein und etwas gefunden haben. 50ms Wartezeit merkt keiner, und Du liest ja nicht von einem Diskettenlaufwerk, also:

                    function handleDropEvent(event) {
                       iterateFilesAndDirs(event.dataTransfer.items, "/");
                       setTimeout(uploadFiles, 50);
                    }
                    
                    function uploadFiles() {
                       if (allFileCount == 0)     // Nichts mehr zu tun
                          return;
                    
                       if (allFiles.length == 0)  // Noch nicht lange genug gewartet
                          setTimeout(uploadFiles, 50);
                    
                       let file = allFiles.shift();
                       // Upload anstoßen, wenn er fertig ist, uploadFiles() neu aufrufen
                    }
                    

                    Zumindest würde ich das auf diese Weise steuern.

                    Rolf

                    --
                    sumpsi - posui - obstruxi
        2. Hallo,

          laut meinem Debugger ist die Methode getFile() nicht vorhanden.

          wenn ich die von Rolf verlinkte Seite richtig verstehe, heißt die gesuchte Methode gar nicht "getfile"…

          Gruß
          Kalk

  2. Jetzt möchte ich das Tool erweitern, sodass auch komplette Ordner hochgeladen werden können.

    Da ich die Struktur beibehalten möchte/muss, bin ich anstatt auf dataTransfer.files auf dataTransfer.items gewechselt. Hier bekomme ich auch den Pfad und kann mir rekursiv alle Dateien erarbeiten.

    Wenn Du file.slice() brauchst muss Du einen geeigneten Blob haben.

    Nun möchte ich aber weiterhin, dass die Dateien in kleine Häppchen zerlegt hochgeladen werden. Leider funktioniert hier das von mir verwendete file.slice(start, ende) nicht mehr.

    Tja. Zurück zu dataTransfer.files ... Ich weiß nicht wie, aber möglicherweise kannst Du den Krempel, den dataTransfer.items liefert, dann bei dataTransfer.files einbauen. Das geht aber offenbar nur via Drag-Operation:

    Note: The files property of DataTransfer objects can only be accessed from within the drop event. For all other events, the files property will be empty — because its underlying data store will be in a protected mode

    Stellt sich die Frage nach dem Warum.

    Es gibt nämlich tausend (Diese Zahl erweist sich bei kleinlicher Zählung vorhersehbar als stark untertrieben!) gute Gründe, einen Browser allenfalls für einen eher gelegentlichen Upload einzelner Dateien zu benutzen. Ein Grund ist das HTTP-Protokoll. Ein anderer, dass Webserver per se darauf ausgelegt sind, vielen Benutzern wenig Leistung (Rechenzeit, Speicher, ...) zu bieten, statt wenigen viel.

    Für den Transfer ganzer Verzeichnisse gibt es andere und bessere sowie sicherere Tools, mit denen man das gestellte Ziel auch ganz einfach erreichen kann. Fangen wir für die „Mausschubser“ mit WinSCP an… Ich persönlich mag rsync oder sshfs. Das ganze Geraffel mit den Versuchen diverser Cloudianer, Dateitransfers mit einem Webrowser zu veranstalten, führte jedenfalls bei mir noch nie zu einem „positiven Nutzungserlebnis“. Und bei jedem, bei dem das so war, würde ich sagen, dass seine Ansprüche an Sicherheit, Performance und Bedienbarkeit mangels besseren Wissens äußerst niedrig liegen. Die Generation Smartphone...

    Anders ausgedrückt: Ganze Horden von Millennials können in der grünen Brühe baden, die ich wegen sowas schon ausgekotzt habe.

    1. Hallo Raketenwilli,

      dataTransfer.files einbauen. Das geht aber offenbar nur via Drag-Operation

      Jein. Du hast recht, was das files-Property des dataTransfer Objekts angeht.

      A-bär... da tut sich was.

      Zum einen kommst Du über die items-Auflistung an FileSystemEntry Objekte, die es auch im Fuchs zu geben scheint.

      Zum anderen unterstützen Chromia und Safari das File System Access API, von dem man ein FileSystemFileHandle oder ein FileSystemDirectoryHandle bekommen kann.

      Darüber geht eine ganze Menge. Sofern die Seite per https serviert wurde.

      Ob ein Upload-Controller im Browser ideal ist, ja, das ist eine andere Frage. Aber wenn man nur einen Webserver hat und keinen anderen Serverzugang…

      Rolf

      --
      sumpsi - posui - obstruxi
      1. Aber wenn man nur einen Webserver hat und keinen anderen Serverzugang…

        Naja… wenngleich eher selten, aber auch dafür gibt es mit WebDAV eine halbwegs benutzbare Lösung und jede Menge Clients:

        Serverseitig:

        Windows:

        • WinSCP
        • FileZilla Pro

        Linux:

        apt list *davfs*
        davfs2/jammy 1.6.1-1 arm64
        davfs2/jammy 1.6.1-1 armhf
        
    2. Stellt sich die Frage nach dem Warum.

      Es gibt nämlich tausend (Diese Zahl erweist sich bei kleinlicher Zählung vorhersehbar als stark untertrieben!) gute Gründe, einen Browser allenfalls für einen eher gelegentlichen Upload einzelner Dateien zu benutzen. Ein Grund ist das HTTP-Protokoll. Ein anderer, dass Webserver per se darauf ausgelegt sind, vielen Benutzern wenig Leistung (Rechenzeit, Speicher, ...) zu bieten, statt wenigen viel.

      Diese Frage möchte ich gerne beantworten...

      Szenario: Unsere Mitarbeiter müssen sehr oft größere Dateien externen Empfängern zur Verfügung stellen. (Die Marketing den Agenturen, die Einkäufer den Lieferanten, etc.) Sehr oft wurde das (und wird immer noch) per Mail versucht. Das führte zwangsläufig zu vielen NDRs (Non-Delivery-Reports). Damit auf der anderen Seite nicht solche Tools wie WeTransfer genutzt werden, wo niemand weiß was mit den Daten passiert, möchten wir ein ähnliches Tool eben selbst anbieten. Datei(en) hochladen, einen Link bekommen, diesen dem Empfänger übermitteln und dieser kann die Dateien bequem wieder herunterladen. Vorteil: Die Empfänger haben i.d.R. Vertaulichkeitsvereinbarungen unterzeichnet und wir bekommen eher mit, was wann hochgeladen und wie oft von wo heruntergeladen wurde.

      1. Hallo Steffen,

        habt ihr schonmal nach deutschland- oder europabasierenden Cloudspeicherdiensten geschaut? Also analog zu irgendwas-drive Diensten?

        Der kostet Geld, aber einen solchen Dienst selbst zu programmieren auch. Dafür bekommt ihr vom Anbieter auch Support, Backup, Authentifizierung, etc.

        Die gibt's wie Sand am Meer und ich kann mir nicht vorstellen, dass das teurer ist als so ein Teil selbst zu programmieren und die Verantwortung dafür zu tragen.

        Produktempfehlungen gebe ich keine ab.

        Rolf

        --
        sumpsi - posui - obstruxi
      2. Hallo

        Stellt sich die Frage nach dem Warum.

        Diese Frage möchte ich gerne beantworten...

        Szenario: Unsere Mitarbeiter müssen sehr oft größere Dateien externen Empfängern zur Verfügung stellen. (Die Marketing den Agenturen, die Einkäufer den Lieferanten, etc.) Sehr oft wurde das (und wird immer noch) per Mail versucht. Das führte zwangsläufig zu vielen NDRs (Non-Delivery-Reports). Damit auf der anderen Seite nicht solche Tools wie WeTransfer genutzt werden, wo niemand weiß was mit den Daten passiert, möchten wir ein ähnliches Tool eben selbst anbieten. Datei(en) hochladen, einen Link bekommen, diesen dem Empfänger übermitteln und dieser kann die Dateien bequem wieder herunterladen.

        Dafür bietet sich eine selbst- oder zumindest in DE/EU fremdgehostete Cloudlösung, wie z.B. NextCloud oder OwnCloud, an. Damit lassen sich über Plugins (auch für Outlook) E-Mail-Anhänge als Downloadlinks versenden, aber auch in der Clioud gespeicherte Dateien und Verzeichnisse an firmeninterne Benutzer aber auch per E-Mail an externe Partner freigeben.

        Tschö, Auge

        --
        200 ist das neue 35.