Dateien verwalten mit Modul jdk.httpserver
ulrich
2024-01-15 cc154b2eb849be9da34f2eca21fa128ae26b53ff
commit | author | age
7fdd7e 1 /*
U 2   http-cm - File management extensions to jdk.httpserver
3   Copyright (C) 2021  Ulrich Hilger
4
5   This program is free software: you can redistribute it and/or modify
6   it under the terms of the GNU Affero General Public License as
7   published by the Free Software Foundation, either version 3 of the
8   License, or (at your option) any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU Affero General Public License for more details.
14
15   You should have received a copy of the GNU Affero General Public License
16   along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  */
18 package de.uhilger.httpserver.cm;
19
8e6840 20 import de.uhilger.httpserver.cm.actor.Zipper;
U 21 import de.uhilger.httpserver.cm.actor.Eraser;
22 import de.uhilger.httpserver.cm.actor.Unzipper;
23 import de.uhilger.httpserver.cm.actor.Renamer;
7fdd7e 24 import com.google.gson.Gson;
U 25 import com.sun.net.httpserver.Authenticator;
26 import com.sun.net.httpserver.Headers;
27 import com.sun.net.httpserver.HttpExchange;
28 import de.uhilger.httpserver.base.HttpResponder;
29 import de.uhilger.httpserver.base.HttpHelper;
30 import de.uhilger.httpserver.base.handler.FileHandler;
cc154b 31 import de.uhilger.httpserver.cm.actor.Lister;
U 32 import de.uhilger.httpserver.cm.actor.Mover;
7fdd7e 33 import de.uhilger.httpserver.image.Datei;
U 34 import de.uhilger.httpserver.image.ImageActor;
35 import de.uhilger.httpserver.image.ImageThread;
36 import de.uhilger.httpserver.image.ImageThread.ThreadListener;
37 import de.uhilger.httpserver.oauth.BearerAuthenticator;
38 import java.io.BufferedReader;
39 import java.io.File;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.InputStreamReader;
44 import java.io.OutputStream;
45 import java.net.URLDecoder;
46 import java.nio.file.DirectoryStream;
47 import java.nio.file.Files;
48 import java.nio.file.Path;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.List;
52 import java.util.logging.Logger;
53 import java.util.logging.Level;
54
55 /**
56  * <p>Der FileManager verknuepft einen HTTP-Endpunkt mit einem Ordner des lokalen
57  * Dateisystems.</p>
58  *
59  * <p>HTTP GET fuer eine Datei innerhalb dieses Ordners liefert den Dateiinhalt aus</p>
60  *
61  * <p>HTTP GET fuer einen Ordner liefert eine Liste von dessen Inhalt in JSON</p>
62  *
63  * <p>HTTP PUT fuer eine Datei ueberschreibt eine bestehende Datei mit dem im Body
64  * uebergebenen Inhalt oder legt eine Datei mit diesem Inhalt an</p>
65  *
66  * <p>HTTP POST fuer eine Datei legt eine neue Datei mit dem im Body uebergebenen
67  * Inhalt an oder erzeugt eine neue Datei mit einer laufenden Nummer, falls
68  * diese Datei schon existiert</p>
69  *
70  * <p>HTTP POST fuer einen Ordner legt einen neuen Ordner an wenn er noch nicht
71  * existiert oder erzeugt einen HTTP-Fehler 422</p>
72  *
73  * <p>HTTP DELETE loescht die Liste der Dateien und Ordner im Body</p>
74  *
75  * <p>HTTP PUT ?copyFrom=pfad kopiert die Liste der Datei- oder Ordnernamen im Body
76  * der Anfrage vom Pfad in 'copyFrom' zum Pfad dieser Anfrage. Jede Datei, die
77  * im Ziel bereits existiert, bekommt im Ziel einen neuen Namen mit einer
78  * laufenden Nummer. Bei Ordnern, die im Ziel bereits existieren, bekommt der
79  * betreffende Ordner im Ziel zunaechst einen neuen Namen mit einer laufenden
80  * Nummer, dann wird der Quellordner ans Ziel kopiert.</p>
81  *
82  * <p>HTTP PUT ?moveFrom=pfad verschiebt die Liste der Datei- oder Ordnernamen im
83  * Body der Anfrage vom Pfad in 'moveFrom' zum Pfad dieser Anfrage. Jede Datei,
84  * die im Ziel bereits existiert, bekommt im Ziel einen neuen Namen mit einer
85  * laufenden Nummer. Bei Ordnern, die im Ziel bereits existieren, bekommt der
86  * betreffende Ordner im Ziel zunaechst einen neuen Namen mit einer laufenden
87  * Nummer, dann wird der Quellordner ans Ziel kopiert.</p>
88  *
89  * <p>HTTP PUT mit ?duplicate legt eine Kopie der Datei an</p>
90  *
91  * <p>HTTP PUT mit '?renameTo=neuer Name' benennt die Datei oder den Ordner um,
92  * sofern der neue Name noch nicht vergeben ist</p>
93  *
94  * <p>HTTP PUT mit '?zip' packt den Ordner</p>
95  *
96  * <p>HTTP PUT mit '?unzip' entpackt eine Datei</p>
97  * 
98  * <p>Namenskonventionen:<br>
99  * Ein Pfad mit Schraegstrich ('/') am Ende bezeichnet einen Ordner<br>
100  * Ein Pfad ohne Schraegstrich ('/') am Ende bezeichnet eine Datei</p>
101  *
102  * @author Ulrich Hilger
103  * @version 1, 13. Mai 2021
104  */
cc154b 105 public class FileManager extends FileHandler {
7fdd7e 106
U 107   /*
108   private static final String[] specialChars = {new String("\u00c4"), new String("\u00d6"),
109     new String("\u00dc"), new String("\u00e4"), new String("\u00f6"), new String("\u00fc"), new String("\u00df")};
110   */
111   
112   //public static final String UNWANTED_PATTERN = "[^a-zA-Z_0-9 ]";
113   /* HTTP Methoden */
114
115   public static final String UTF8 = "UTF-8";
116
117   public static final String STR_SLASH = "/";
118   public static final String STR_DOT = ".";
119   
120   public static final String P_COPY = "copyFrom";
121   public static final String P_MOVE = "moveFrom";
122   public static final String P_DUPLICATE = "duplicate";
123   public static final String P_RENAME = "renameTo";
124   public static final String P_ZIP = "zip";
125   public static final String P_UNZIP = "unzip";
126
127   public static final int OP_COPY = 1;
128   public static final int OP_MOVE = 2;
129   public static final int OP_DELETE = 3;
130   
131   public static final String ATTR_ROLE = "role";
132   
133   //private String role;
134
135   //public FileManager(String absoluteDirectoryPathAndName, String role, String ctx) {
136   public FileManager() {
137     //super(absoluteDirectoryPathAndName, ctx);
138     //super(absoluteDirectoryPathAndName);
139     //this.role = role;
140   }
141
142   @Override
143   public void handle(HttpExchange e) throws IOException {
144     Authenticator a = e.getHttpContext().getAuthenticator();
145     if(a instanceof BearerAuthenticator) {
146       BearerAuthenticator auth = (BearerAuthenticator) a;
147       //Realm realm = auth.getRealm();
148       String userId = e.getPrincipal().getUsername();
149       if(auth.hasRole(userId, e.getHttpContext().getAttributes().get(ATTR_ROLE).toString())) {
150         String method = e.getRequestMethod();
5adf10 151         //logger.fine("method: " + method);
7fdd7e 152         HttpHelper helper = new HttpHelper();
U 153         switch (method) {
154           case HttpHelper.HTTP_GET:
cc154b 155             String path = e.getRequestURI().toString();
U 156             if (path.endsWith(STR_SLASH)) {
157               String json = new Lister().liste(helper.getFileName(e), 
158                       e.getHttpContext().getPath(), 
159                       e.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString(),
160                       path);
161               if(null != json) {
162                 HttpResponder r = new HttpResponder();
163                 r.antwortSenden(e, SC_OK, json);              
164               } else {
165                 emptyListResponse(e);    
166               }
167             } else {
168               new Lister().b64Action(helper.getFileName(e), 
169                       e.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
170               super.handle(e);
171             }
7fdd7e 172             break;
U 173           case HttpHelper.HTTP_PUT:
174             put(e, helper);
175             break;
176           case HttpHelper.HTTP_POST:
177             speichern(e, helper);
178             break;
179           case HttpHelper.HTTP_DELETE:
180             loeschen(e, helper);
181             break;
182         }
183       } else {
184         standardHeaderUndAntwort(e, SC_FORBIDDEN, "Fehlende Rolle.");
185       }
186     } else {
187       standardHeaderUndAntwort(e, SC_FORBIDDEN, "Fehlende Rolle.");
188     }
189   }
190
191   private void put(HttpExchange exchange, HttpHelper helper) throws IOException {
192     String query = exchange.getRequestURI().getQuery();
193     if (query != null) {
194       String[] params = query.split("=");
195       for (String param : params) {
5adf10 196         //logger.fine("param: " + param);
7fdd7e 197       }
U 198       switch (params[0]) {
199         case P_COPY:
200           copyOrMove(exchange, params[1], helper.getFileName(exchange), OP_COPY);
201           break;
202         case P_MOVE:
203           copyOrMove(exchange, params[1], helper.getFileName(exchange), OP_MOVE);
204           break;
205         case P_DUPLICATE:
206           if(Boolean.parseBoolean(params[1])) {
cc154b 207             String neuerDateiName = new Mover().duplizieren(
U 208                     exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString(), 
209                     helper.getFileName(exchange));
5adf10 210             //logger.fine("neuer Name: " + neuerDateiName);
7fdd7e 211             standardHeaderUndAntwort(exchange, SC_OK, neuerDateiName);
U 212           }
213           break;
214         case P_RENAME:
8e6840 215           String neuerDateiName = new Renamer().umbenennen(exchange, helper, params[1]);
5adf10 216           //logger.fine("neuer Name: " + neuerDateiName);
7fdd7e 217           standardHeaderUndAntwort(exchange, SC_OK, neuerDateiName);
U 218           break;
219         case P_ZIP:
220           String path = exchange.getRequestURI().toString();
5adf10 221           //logger.fine(path);
020a97 222           String antwort = new Zipper().packFolder(helper.getFileName(exchange), path, exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
7fdd7e 223           if(antwort.equalsIgnoreCase("ok")) {
U 224             standardHeaderUndAntwort(exchange, SC_OK, antwort);
225           } else {
226             standardHeaderUndAntwort(exchange, SC_UNPROCESSABLE_ENTITY, antwort);
227           }
228           break;
229         case P_UNZIP:
230           path = exchange.getRequestURI().toString();
5adf10 231           //logger.fine(path);
020a97 232           antwort = new Unzipper().extractZipfile(helper.getFileName(exchange), path, exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
7fdd7e 233           if(antwort.equalsIgnoreCase("ok")) {
U 234             standardHeaderUndAntwort(exchange, SC_OK, antwort);
235           } else {
236             standardHeaderUndAntwort(exchange, SC_UNPROCESSABLE_ENTITY, antwort);
237           }
238           break;
239       }
240     } else {
241       speichern(exchange, helper);
242     }
243   }
66173f 244   
7fdd7e 245   
U 246   private void emptyListResponse(HttpExchange e) throws IOException {
247     HttpResponder r = new HttpResponder();
248     String json = "{}";
5adf10 249     //logger.log(Level.FINE, "json: ''{0}''", json);
7fdd7e 250     r.antwortSenden(e, SC_OK, json);        
U 251   }
252
253   private void speichern(HttpExchange exchange, HttpHelper helper) throws IOException {
254     String fileName = helper.getFileName(exchange);
5adf10 255     //logger.info("fileName: " + fileName);
7fdd7e 256
U 257     // file ist die Datei, um die es geht
258     File file = new File(exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString(), fileName);
259
260     String method = exchange.getRequestMethod();
261     if (fileName.endsWith(STR_SLASH)) {
5adf10 262       //logger.info("neuer Ordner: " + file.getAbsolutePath());
7fdd7e 263       // neuen Ordner erstellen oder ablehnen, wenn der Ordner schon existiert
U 264       if (method.equalsIgnoreCase(HttpHelper.HTTP_POST)) {
265         if (!file.exists()) {
266           file.mkdir();
267           standardHeaderUndAntwort(exchange, SC_OK, file.getAbsolutePath());
268         } else {
269           String antwort = "Ordner existiert bereits.";
270           standardHeaderUndAntwort(exchange, SC_UNPROCESSABLE_ENTITY, antwort);
271         }
272       } else {
273         String antwort = "PUT fuer neuen Ordner nicht erlaubt, bitte POST verwenden.";
274         standardHeaderUndAntwort(exchange, SC_METHOD_NOT_ALLOWED, antwort);        
275       }
276     } else {
5adf10 277       //logger.info("Datei speichern: " + file.getAbsolutePath());
7fdd7e 278       // Datei speichern
U 279       if (method.equalsIgnoreCase(HttpHelper.HTTP_POST)) {
280         if (file.exists()) {
281           FileTransporter trans = new FileTransporter();
282           file = trans.getNewFileName(file);
283         }
284       } else if (method.equalsIgnoreCase(HttpHelper.HTTP_PUT)) {
285         if (file.exists()) {
286           /*
287             muss delete() sein?
288             pruefen: ueberschreibt der FileWriter den alteen Inhalt oder 
289             entsteht eine unerwuenschte Mischung aus altem und neuem 
290             Inhalt?
291            */
292           file.delete();
293         } else {
294           file.getParentFile().mkdirs();
295         }
296       }
297       // Request Body mit dem Dateiinhalt in einen String lesen
298       StringBuilder sb = new StringBuilder();
299       InputStream is = exchange.getRequestBody();
300       BufferedReader in = new BufferedReader(new InputStreamReader(is));
301       String line = in.readLine();
302       while (line != null) {
303         sb.append(line);
304         line = in.readLine();
305       }
306
307       // dekodieren
308       String content = sb.toString();
5adf10 309       //logger.fine(content);
7fdd7e 310       String decoded = URLDecoder.decode(content, UTF8);
5adf10 311       //logger.fine(decoded);
7fdd7e 312
U 313       // in Datei schreiben
314       byte[] bytes = decoded.getBytes();
315       file.createNewFile();
316       OutputStream os = new FileOutputStream(file);
317       os.write(bytes);
318       os.flush();
319       os.close();
320       is.close();
321
322       // Antwort senden
323       standardHeaderUndAntwort(exchange, SC_OK, file.getAbsolutePath());
324     }
325   }
326
327   private void copyOrMove(HttpExchange exchange, String quelle, String ziel, int op) throws IOException {
5adf10 328     //logger.fine("quelle: " + quelle + ", ziel: " + ziel);
7fdd7e 329     String[] dateiNamen = dateiliste(exchange);
cc154b 330     new Mover().copyOrMoveFiles(quelle, ziel, dateiNamen, op, exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
7fdd7e 331     standardHeaderUndAntwort(exchange, SC_OK, "Dateien verarbeitet.");
U 332   }
333
334   private void loeschen(HttpExchange exchange, HttpHelper helper) throws IOException {
335     String[] dateiNamen = dateiliste(exchange);
336     String relPfad = helper.getFileName(exchange);
8e6840 337     new Eraser().deleteFiles(relPfad, Arrays.asList(dateiNamen), exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
7fdd7e 338     standardHeaderUndAntwort(exchange, SC_OK, "Dateien geloescht.");
U 339   }
340
341   private String[] dateiliste(HttpExchange exchange) throws IOException {
342     String body = new HttpHelper().bodyLesen(exchange);
5adf10 343     //logger.fine("dateien: " + body);
7fdd7e 344     Gson gson = new Gson();
U 345     return gson.fromJson(body, String[].class);
346   }
347
348
349   private void standardHeaderUndAntwort(HttpExchange exchange, int status, String antwort) throws IOException {
350     Headers resHeaders = exchange.getResponseHeaders();
351     resHeaders.add(CONTENT_TYPE, HttpHelper.CT_TEXT_HTML);
352     new HttpResponder().antwortSenden(exchange, status, antwort);
353   }  
354 }