All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.activej.fs.http.ActiveFsServlet Maven / Gradle / Ivy

Go to download

Provides tools for building efficient, scalable local, remote or clustered file servers. It utilizes ActiveJ CSP for fast and reliable file transfer.

There is a newer version: 6.0-beta2
Show newest version
/*
 * Copyright (C) 2020 ActiveJ LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.activej.fs.http;

import io.activej.bytebuf.ByteBuf;
import io.activej.common.function.FunctionEx;
import io.activej.common.initializer.WithInitializer;
import io.activej.csp.ChannelConsumer;
import io.activej.csp.ChannelSupplier;
import io.activej.fs.ActiveFs;
import io.activej.fs.exception.FileNotFoundException;
import io.activej.fs.exception.FsException;
import io.activej.http.*;
import io.activej.http.MultipartDecoder.MultipartDataHandler;
import io.activej.promise.Promise;
import org.jetbrains.annotations.NotNull;

import java.util.Objects;

import static io.activej.common.function.FunctionEx.identity;
import static io.activej.fs.http.FsCommand.*;
import static io.activej.fs.util.MessageTypes.STRING_SET_TYPE;
import static io.activej.fs.util.MessageTypes.STRING_STRING_MAP_TYPE;
import static io.activej.fs.util.RemoteFsUtils.*;
import static io.activej.http.ContentTypes.JSON_UTF_8;
import static io.activej.http.ContentTypes.PLAIN_TEXT_UTF_8;
import static io.activej.http.HttpHeaderValue.ofContentType;
import static io.activej.http.HttpHeaders.*;
import static io.activej.http.HttpMethod.GET;
import static io.activej.http.HttpMethod.POST;

/**
 * An HTTP servlet that exposes some given {@link ActiveFs}.
 * 

* Servlet is fully compatible with {@link HttpActiveFs} client. *

* It also defines additional endpoints that can be useful for accessing via web browser, * such as uploading multiple files using multipart/form-data content type * and downloading a file using range requests. *

* This server may be launched as a publicly available server. */ public final class ActiveFsServlet implements WithInitializer { private ActiveFsServlet() { } public static RoutingServlet create(ActiveFs fs) { return create(fs, true); } public static RoutingServlet create(ActiveFs fs, boolean inline) { return RoutingServlet.create() .map(POST, "/" + UPLOAD + "/*", request -> { String contentLength = request.getHeader(CONTENT_LENGTH); Long size = contentLength == null ? null : Long.valueOf(contentLength); return (size == null ? fs.upload(decodePath(request)) : fs.upload(decodePath(request), size)) .map(uploadAcknowledgeFn(request), errorResponseFn()); }) .map(POST, "/" + UPLOAD, request -> request.handleMultipart(MultipartDataHandler.file(fs::upload)) .map(voidResponseFn(), errorResponseFn())) .map(POST, "/" + APPEND + "/*", request -> { long offset = getNumberParameterOr(request, "offset", 0); return fs.append(decodePath(request), offset) .map(uploadAcknowledgeFn(request), errorResponseFn()); }) .map(GET, "/" + DOWNLOAD + "/*", request -> { String name = decodePath(request); String rangeHeader = request.getHeader(HttpHeaders.RANGE); if (rangeHeader != null) { return rangeDownload(fs, inline, name, rangeHeader); } long offset = getNumberParameterOr(request, "offset", 0); long limit = getNumberParameterOr(request, "limit", Long.MAX_VALUE); return fs.download(name, offset, limit) .map(res -> HttpResponse.ok200() .withHeader(ACCEPT_RANGES, "bytes") .withBodyStream(res), errorResponseFn()); }) .map(GET, "/" + LIST, request -> { String glob = request.getQueryParameter("glob"); glob = glob != null ? glob : "**"; return fs.list(glob) .map(list -> HttpResponse.ok200() .withBody(toJson(list)) .withHeader(CONTENT_TYPE, ofContentType(JSON_UTF_8)), errorResponseFn()); }) .map(GET, "/" + INFO + "/*", request -> fs.info(decodePath(request)) .map(meta -> HttpResponse.ok200() .withBody(toJson(meta)) .withHeader(CONTENT_TYPE, ofContentType(JSON_UTF_8)), errorResponseFn())) .map(GET, "/" + INFO_ALL, request -> request.loadBody() .map(body -> fromJson(STRING_SET_TYPE, body)) .then(fs::infoAll) .map(map -> HttpResponse.ok200() .withBody(toJson(map)) .withHeader(CONTENT_TYPE, ofContentType(JSON_UTF_8)), errorResponseFn())) .map(GET, "/" + PING, request -> fs.ping() .map(voidResponseFn(), errorResponseFn())) .map(POST, "/" + MOVE, request -> { String name = getQueryParameter(request, "name"); String target = getQueryParameter(request, "target"); return fs.move(name, target) .map(voidResponseFn(), errorResponseFn()); }) .map(POST, "/" + MOVE_ALL, request -> request.loadBody() .map(body -> fromJson(STRING_STRING_MAP_TYPE, body)) .then(fs::moveAll) .map(voidResponseFn(), errorResponseFn())) .map(POST, "/" + COPY, request -> { String name = getQueryParameter(request, "name"); String target = getQueryParameter(request, "target"); return fs.copy(name, target) .map(voidResponseFn(), errorResponseFn()); }) .map(POST, "/" + COPY_ALL, request -> request.loadBody() .map(body -> fromJson(STRING_STRING_MAP_TYPE, body)) .then(fs::copyAll) .map(voidResponseFn(), errorResponseFn())) .map(HttpMethod.DELETE, "/" + DELETE + "/*", request -> fs.delete(decodePath(request)) .map(voidResponseFn(), errorResponseFn())) .map(POST, "/" + DELETE_ALL, request -> request.loadBody() .map(body -> fromJson(STRING_SET_TYPE, body)) .then(fs::deleteAll) .map(voidResponseFn(), errorResponseFn())); } private static @NotNull Promise rangeDownload(ActiveFs fs, boolean inline, String name, String rangeHeader) { //noinspection ConstantConditions return fs.info(name) .whenResult(Objects::isNull, $ -> { throw new FileNotFoundException(); }) .then(meta -> HttpResponse.file( (offset, limit) -> fs.download(name, offset, limit), name, meta.getSize(), rangeHeader, inline)) .map(identity(), errorResponseFn()); } private static String decodePath(HttpRequest request) throws HttpError { String value = UrlParser.urlParse(request.getRelativePath()); if (value == null) { throw HttpError.ofCode(400, "Path contains invalid UTF"); } return value; } private static String getQueryParameter(HttpRequest request, String parameterName) throws HttpError { String value = request.getQueryParameter(parameterName); if (value == null) { throw HttpError.ofCode(400, "No '" + parameterName + "' query parameter"); } return value; } private static long getNumberParameterOr(HttpRequest request, String parameterName, long defaultValue) throws HttpError { String value = request.getQueryParameter(parameterName); if (value == null) { return defaultValue; } try { long val = Long.parseLong(value); if (val < 0) { throw new NumberFormatException(); } return val; } catch (NumberFormatException ignored) { throw HttpError.ofCode(400, "Invalid '" + parameterName + "' value"); } } private static FunctionEx errorResponseFn() { return e -> HttpResponse.ofCode(500) .withHeader(CONTENT_TYPE, ofContentType(JSON_UTF_8)) .withBody(toJson(FsException.class, castError(e))); } private static FunctionEx voidResponseFn() { return $ -> HttpResponse.ok200().withHeader(CONTENT_TYPE, ofContentType(PLAIN_TEXT_UTF_8)); } private static FunctionEx, HttpResponse> uploadAcknowledgeFn(@NotNull HttpRequest request) { return consumer -> HttpResponse.ok200() .withHeader(CONTENT_TYPE, ofContentType(JSON_UTF_8)) .withBodyStream(ChannelSupplier.ofPromise(request.takeBodyStream() .streamTo(consumer) .map($ -> UploadAcknowledgement.ok(), e -> UploadAcknowledgement.ofError(castError(e))) .map(ack -> ChannelSupplier.of(toJson(ack))))); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy