/*
http-cm - File management extensions to jdk.httpserver
Copyright (C) 2021 Ulrich Hilger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package de.uhilger.httpserver.cm;
import de.uhilger.httpserver.cm.actor.Zipper;
import de.uhilger.httpserver.cm.actor.Eraser;
import de.uhilger.httpserver.cm.actor.Unzipper;
import de.uhilger.httpserver.cm.actor.Renamer;
import com.google.gson.Gson;
import com.sun.net.httpserver.Authenticator;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import de.uhilger.httpserver.base.HttpResponder;
import de.uhilger.httpserver.base.HttpHelper;
import de.uhilger.httpserver.base.handler.FileHandler;
import de.uhilger.httpserver.cm.actor.Lister;
import de.uhilger.httpserver.cm.actor.Mover;
import de.uhilger.httpserver.image.Datei;
import de.uhilger.httpserver.image.ImageActor;
import de.uhilger.httpserver.image.ImageThread;
import de.uhilger.httpserver.image.ImageThread.ThreadListener;
import de.uhilger.httpserver.oauth.BearerAuthenticator;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.logging.Level;
/**
*
Der FileManager verknuepft einen HTTP-Endpunkt mit einem Ordner des lokalen
* Dateisystems.
*
* HTTP GET fuer eine Datei innerhalb dieses Ordners liefert den Dateiinhalt aus
*
* HTTP GET fuer einen Ordner liefert eine Liste von dessen Inhalt in JSON
*
* HTTP PUT fuer eine Datei ueberschreibt eine bestehende Datei mit dem im Body
* uebergebenen Inhalt oder legt eine Datei mit diesem Inhalt an
*
* HTTP POST fuer eine Datei legt eine neue Datei mit dem im Body uebergebenen
* Inhalt an oder erzeugt eine neue Datei mit einer laufenden Nummer, falls
* diese Datei schon existiert
*
* HTTP POST fuer einen Ordner legt einen neuen Ordner an wenn er noch nicht
* existiert oder erzeugt einen HTTP-Fehler 422
*
* HTTP DELETE loescht die Liste der Dateien und Ordner im Body
*
* HTTP PUT ?copyFrom=pfad kopiert die Liste der Datei- oder Ordnernamen im Body
* der Anfrage vom Pfad in 'copyFrom' zum Pfad dieser Anfrage. Jede Datei, die
* im Ziel bereits existiert, bekommt im Ziel einen neuen Namen mit einer
* laufenden Nummer. Bei Ordnern, die im Ziel bereits existieren, bekommt der
* betreffende Ordner im Ziel zunaechst einen neuen Namen mit einer laufenden
* Nummer, dann wird der Quellordner ans Ziel kopiert.
*
* HTTP PUT ?moveFrom=pfad verschiebt die Liste der Datei- oder Ordnernamen im
* Body der Anfrage vom Pfad in 'moveFrom' zum Pfad dieser Anfrage. Jede Datei,
* die im Ziel bereits existiert, bekommt im Ziel einen neuen Namen mit einer
* laufenden Nummer. Bei Ordnern, die im Ziel bereits existieren, bekommt der
* betreffende Ordner im Ziel zunaechst einen neuen Namen mit einer laufenden
* Nummer, dann wird der Quellordner ans Ziel kopiert.
*
* HTTP PUT mit ?duplicate legt eine Kopie der Datei an
*
* HTTP PUT mit '?renameTo=neuer Name' benennt die Datei oder den Ordner um,
* sofern der neue Name noch nicht vergeben ist
*
* HTTP PUT mit '?zip' packt den Ordner
*
* HTTP PUT mit '?unzip' entpackt eine Datei
*
* Namenskonventionen:
* Ein Pfad mit Schraegstrich ('/') am Ende bezeichnet einen Ordner
* Ein Pfad ohne Schraegstrich ('/') am Ende bezeichnet eine Datei
*
* @author Ulrich Hilger
* @version 1, 13. Mai 2021
*/
public class FileManager extends FileHandler {
/*
private static final String[] specialChars = {new String("\u00c4"), new String("\u00d6"),
new String("\u00dc"), new String("\u00e4"), new String("\u00f6"), new String("\u00fc"), new String("\u00df")};
*/
//public static final String UNWANTED_PATTERN = "[^a-zA-Z_0-9 ]";
/* HTTP Methoden */
public static final String UTF8 = "UTF-8";
public static final String STR_SLASH = "/";
public static final String STR_DOT = ".";
public static final String P_COPY = "copyFrom";
public static final String P_MOVE = "moveFrom";
public static final String P_DUPLICATE = "duplicate";
public static final String P_RENAME = "renameTo";
public static final String P_ZIP = "zip";
public static final String P_UNZIP = "unzip";
public static final int OP_COPY = 1;
public static final int OP_MOVE = 2;
public static final int OP_DELETE = 3;
public static final String ATTR_ROLE = "role";
//private String role;
//public FileManager(String absoluteDirectoryPathAndName, String role, String ctx) {
public FileManager() {
//super(absoluteDirectoryPathAndName, ctx);
//super(absoluteDirectoryPathAndName);
//this.role = role;
}
@Override
public void handle(HttpExchange e) throws IOException {
Authenticator a = e.getHttpContext().getAuthenticator();
if(a instanceof BearerAuthenticator) {
BearerAuthenticator auth = (BearerAuthenticator) a;
//Realm realm = auth.getRealm();
String userId = e.getPrincipal().getUsername();
if(auth.hasRole(userId, e.getHttpContext().getAttributes().get(ATTR_ROLE).toString())) {
String method = e.getRequestMethod();
//logger.fine("method: " + method);
HttpHelper helper = new HttpHelper();
switch (method) {
case HttpHelper.HTTP_GET:
String path = e.getRequestURI().toString();
if (path.endsWith(STR_SLASH)) {
String json = new Lister().liste(helper.getFileName(e),
e.getHttpContext().getPath(),
e.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString(),
path);
if(null != json) {
HttpResponder r = new HttpResponder();
r.antwortSenden(e, SC_OK, json);
} else {
emptyListResponse(e);
}
} else {
new Lister().b64Action(helper.getFileName(e),
e.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
super.handle(e);
}
break;
case HttpHelper.HTTP_PUT:
put(e, helper);
break;
case HttpHelper.HTTP_POST:
speichern(e, helper);
break;
case HttpHelper.HTTP_DELETE:
loeschen(e, helper);
break;
}
} else {
standardHeaderUndAntwort(e, SC_FORBIDDEN, "Fehlende Rolle.");
}
} else {
standardHeaderUndAntwort(e, SC_FORBIDDEN, "Fehlende Rolle.");
}
}
private void put(HttpExchange exchange, HttpHelper helper) throws IOException {
String query = exchange.getRequestURI().getQuery();
if (query != null) {
String[] params = query.split("=");
for (String param : params) {
//logger.fine("param: " + param);
}
switch (params[0]) {
case P_COPY:
copyOrMove(exchange, params[1], helper.getFileName(exchange), OP_COPY);
break;
case P_MOVE:
copyOrMove(exchange, params[1], helper.getFileName(exchange), OP_MOVE);
break;
case P_DUPLICATE:
if(Boolean.parseBoolean(params[1])) {
String neuerDateiName = new Mover().duplizieren(
exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString(),
helper.getFileName(exchange));
//logger.fine("neuer Name: " + neuerDateiName);
standardHeaderUndAntwort(exchange, SC_OK, neuerDateiName);
}
break;
case P_RENAME:
String neuerDateiName = new Renamer().umbenennen(exchange, helper, params[1]);
//logger.fine("neuer Name: " + neuerDateiName);
standardHeaderUndAntwort(exchange, SC_OK, neuerDateiName);
break;
case P_ZIP:
String path = exchange.getRequestURI().toString();
//logger.fine(path);
String antwort = new Zipper().packFolder(helper.getFileName(exchange), path, exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
if(antwort.equalsIgnoreCase("ok")) {
standardHeaderUndAntwort(exchange, SC_OK, antwort);
} else {
standardHeaderUndAntwort(exchange, SC_UNPROCESSABLE_ENTITY, antwort);
}
break;
case P_UNZIP:
path = exchange.getRequestURI().toString();
//logger.fine(path);
antwort = new Unzipper().extractZipfile(helper.getFileName(exchange), path, exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
if(antwort.equalsIgnoreCase("ok")) {
standardHeaderUndAntwort(exchange, SC_OK, antwort);
} else {
standardHeaderUndAntwort(exchange, SC_UNPROCESSABLE_ENTITY, antwort);
}
break;
}
} else {
speichern(exchange, helper);
}
}
private void emptyListResponse(HttpExchange e) throws IOException {
HttpResponder r = new HttpResponder();
String json = "{}";
//logger.log(Level.FINE, "json: ''{0}''", json);
r.antwortSenden(e, SC_OK, json);
}
private void speichern(HttpExchange exchange, HttpHelper helper) throws IOException {
String fileName = helper.getFileName(exchange);
//logger.info("fileName: " + fileName);
// file ist die Datei, um die es geht
File file = new File(exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString(), fileName);
String method = exchange.getRequestMethod();
if (fileName.endsWith(STR_SLASH)) {
//logger.info("neuer Ordner: " + file.getAbsolutePath());
// neuen Ordner erstellen oder ablehnen, wenn der Ordner schon existiert
if (method.equalsIgnoreCase(HttpHelper.HTTP_POST)) {
if (!file.exists()) {
file.mkdir();
standardHeaderUndAntwort(exchange, SC_OK, file.getAbsolutePath());
} else {
String antwort = "Ordner existiert bereits.";
standardHeaderUndAntwort(exchange, SC_UNPROCESSABLE_ENTITY, antwort);
}
} else {
String antwort = "PUT fuer neuen Ordner nicht erlaubt, bitte POST verwenden.";
standardHeaderUndAntwort(exchange, SC_METHOD_NOT_ALLOWED, antwort);
}
} else {
//logger.info("Datei speichern: " + file.getAbsolutePath());
// Datei speichern
if (method.equalsIgnoreCase(HttpHelper.HTTP_POST)) {
if (file.exists()) {
FileTransporter trans = new FileTransporter();
file = trans.getNewFileName(file);
}
} else if (method.equalsIgnoreCase(HttpHelper.HTTP_PUT)) {
if (file.exists()) {
/*
muss delete() sein?
pruefen: ueberschreibt der FileWriter den alteen Inhalt oder
entsteht eine unerwuenschte Mischung aus altem und neuem
Inhalt?
*/
file.delete();
} else {
file.getParentFile().mkdirs();
}
}
// Request Body mit dem Dateiinhalt in einen String lesen
StringBuilder sb = new StringBuilder();
InputStream is = exchange.getRequestBody();
BufferedReader in = new BufferedReader(new InputStreamReader(is));
String line = in.readLine();
while (line != null) {
sb.append(line);
line = in.readLine();
}
// dekodieren
String content = sb.toString();
//logger.fine(content);
String decoded = URLDecoder.decode(content, UTF8);
//logger.fine(decoded);
// in Datei schreiben
byte[] bytes = decoded.getBytes();
file.createNewFile();
OutputStream os = new FileOutputStream(file);
os.write(bytes);
os.flush();
os.close();
is.close();
// Antwort senden
standardHeaderUndAntwort(exchange, SC_OK, file.getAbsolutePath());
}
}
private void copyOrMove(HttpExchange exchange, String quelle, String ziel, int op) throws IOException {
//logger.fine("quelle: " + quelle + ", ziel: " + ziel);
String[] dateiNamen = dateiliste(exchange);
new Mover().copyOrMoveFiles(quelle, ziel, dateiNamen, op, exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
standardHeaderUndAntwort(exchange, SC_OK, "Dateien verarbeitet.");
}
private void loeschen(HttpExchange exchange, HttpHelper helper) throws IOException {
String[] dateiNamen = dateiliste(exchange);
String relPfad = helper.getFileName(exchange);
new Eraser().deleteFiles(relPfad, Arrays.asList(dateiNamen), exchange.getHttpContext().getAttributes().get(FileHandler.ATTR_FILE_BASE).toString());
standardHeaderUndAntwort(exchange, SC_OK, "Dateien geloescht.");
}
private String[] dateiliste(HttpExchange exchange) throws IOException {
String body = new HttpHelper().bodyLesen(exchange);
//logger.fine("dateien: " + body);
Gson gson = new Gson();
return gson.fromJson(body, String[].class);
}
private void standardHeaderUndAntwort(HttpExchange exchange, int status, String antwort) throws IOException {
Headers resHeaders = exchange.getResponseHeaders();
resHeaders.add(CONTENT_TYPE, HttpHelper.CT_TEXT_HTML);
new HttpResponder().antwortSenden(exchange, status, antwort);
}
}