/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server.http.file; import static java.util.Objects.requireNonNull; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import javax.annotation.Nullable; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.http.HttpData; /** * A virtual file system that provides the files requested by {@link HttpFileService}. */ @FunctionalInterface public interface HttpVfs { /** * Creates a new {@link HttpVfs} with the specified {@code rootDir} in an O/S file system. */ static HttpVfs ofFileSystem(String rootDir) { return new FileSystemHttpVfs(Paths.get(requireNonNull(rootDir, "rootDir"))); } /** * Creates a new {@link HttpVfs} with the specified {@code rootDir} in an O/S file system. */ static HttpVfs ofFileSystem(Path rootDir) { return new FileSystemHttpVfs(rootDir); } /** * Creates a new {@link HttpVfs} with the specified {@code rootDir} in the current class path. */ static HttpVfs ofClassPath(String rootDir) { return ofClassPath(HttpVfs.class.getClassLoader(), rootDir); } /** * Creates a new {@link HttpVfs} with the specified {@code rootDir} in the current class path. */ static HttpVfs ofClassPath(ClassLoader classLoader, String rootDir) { return new ClassPathHttpVfs(classLoader, rootDir); } /** * Finds the file at the specified {@code path}. * * * @param path an absolute path whose component separator is {@code '/'} * @param contentEncoding the content encoding of the file. Will be non-null for precompressed resources * * @return the {@link Entry} of the file at the specified {@code path} if found. * {@link Entry#NONE} if not found. */ Entry get(String path, @Nullable String contentEncoding); /** * A file entry in an {@link HttpVfs}. */ interface Entry { /** * A non-existent entry. */ Entry NONE = new Entry() { @Override public MediaType mediaType() { throw new IllegalStateException(); } @Nullable @Override public String contentEncoding() { return null; } @Override public long lastModifiedMillis() { return 0; } @Override public HttpData readContent() throws IOException { throw new FileNotFoundException(); } @Override public String toString() { return "none"; } }; /** * Returns the MIME type of the entry. * * @return {@code null} if unknown */ MediaType mediaType(); /** * The content encoding of the entry. Will be set for precompressed files. * * @return {code null} if not compressed */ @Nullable String contentEncoding(); /** * Returns the modification time of the entry. * * @return {@code 0} if the entry does not exist. */ long lastModifiedMillis(); /** * Reads the content of the entry into a new buffer. */ HttpData readContent() throws IOException; } /** * A skeletal {@link Entry} implementation. */ abstract class AbstractEntry implements Entry { private final String path; @Nullable private final MediaType mediaType; @Nullable private final String contentEncoding; /** * Creates a new instance with the specified {@code path}. */ protected AbstractEntry(String path, @Nullable String contentEncoding) { this(path, MimeTypeUtil.guessFromPath(path, contentEncoding != null), contentEncoding); } /** * Creates a new instance with the specified {@code path} and {@code mediaType}. */ protected AbstractEntry(String path, @Nullable MediaType mediaType, @Nullable String contentEncoding) { this.path = requireNonNull(path, "path"); this.mediaType = mediaType; this.contentEncoding = contentEncoding; } @Override @Nullable public MediaType mediaType() { return mediaType; } @Override @Nullable public String contentEncoding() { return contentEncoding; } @Override public String toString() { return path; } /** * Reads the content of the entry into a new buffer. * Use {@link #readContent(InputStream, int)} when the length of the stream is known. */ protected HttpData readContent(InputStream in) throws IOException { byte[] buf = new byte[Math.max(in.available(), 1024)]; int endOffset = 0; for (;;) { final int readBytes = in.read(buf, endOffset, buf.length - endOffset); if (readBytes < 0) { break; } endOffset += readBytes; if (endOffset == buf.length) { buf = Arrays.copyOf(buf, buf.length << 1); } } return endOffset != 0 ? HttpData.of(buf, 0, endOffset) : HttpData.EMPTY_DATA; } /** * Reads the content of the entry into a new buffer. * Use {@link #readContent(InputStream)} when the length of the stream is unknown. */ protected HttpData readContent(InputStream in, int length) throws IOException { if (length == 0) { return HttpData.EMPTY_DATA; } byte[] buf = new byte[length]; int endOffset = 0; for (;;) { final int readBytes = in.read(buf, endOffset, buf.length - endOffset); if (readBytes < 0) { break; } endOffset += readBytes; if (endOffset == buf.length) { break; } } return HttpData.of(buf, 0, endOffset); } } /** * An {@link Entry} whose content is backed by a byte array. */ final class ByteArrayEntry extends AbstractEntry { private final long lastModifiedMillis; private final HttpData content; /** * Creates a new instance with the specified {@code path} and byte array. */ public ByteArrayEntry(String path, byte[] content) { this(path, content, System.currentTimeMillis()); } /** * Creates a new instance with the specified {@code path} and byte array. */ public ByteArrayEntry(String path, byte[] content, long lastModifiedMillis) { super(path, null); this.content = HttpData.of(requireNonNull(content, "content")); this.lastModifiedMillis = lastModifiedMillis; } /** * Creates a new instance with the specified {@code path}, {@code mediaType} and byte array. */ public ByteArrayEntry(String path, MediaType mediaType, byte[] content) { this(path, mediaType, content, System.currentTimeMillis()); } /** * Creates a new instance with the specified {@code path}, {@code mediaType} and byte array. */ public ByteArrayEntry(String path, MediaType mediaType, byte[] content, long lastModifiedMillis) { super(path, mediaType, null); this.content = HttpData.of(requireNonNull(content, "content")); this.lastModifiedMillis = lastModifiedMillis; } @Override public long lastModifiedMillis() { return lastModifiedMillis; } @Override public HttpData readContent() { return content; } } }