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

jmms.plugins.sfs.SfsController Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright 2019 the original author or authors.
 *
 *  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 jmms.plugins.sfs;

import leap.core.security.annotation.AllowAnonymous;
import leap.core.security.token.TokenVerifyException;
import leap.core.security.token.jwt.MacSigner;
import leap.lang.Beans;
import leap.lang.New;
import leap.lang.Strings;
import leap.lang.convert.Converts;
import leap.lang.io.IO;
import leap.lang.logging.Log;
import leap.lang.logging.LogFactory;
import leap.lang.util.ShortID;
import leap.lang.util.ShortUUID;
import leap.web.exception.BadRequestException;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/$sfs")
public class SfsController implements InitializingBean {

    private static final Log log = LogFactory.get(SfsController.class);

    @Autowired
    protected SfsConfig                     config;
    protected MacSigner                     signer;
    protected ExpiringMap uploadWaitingCommits;
    protected ExpiringMap downloadWaitingCommits;

    private final static String ASSETS = "assets";

    @Override
    public void afterPropertiesSet() throws Exception {
        signer = new MacSigner(config.getSignSecret());

        uploadWaitingCommits =
                ExpiringMap.builder().expiration(config.getWaitingCommitExpires(), TimeUnit.SECONDS)
                        .maxSize(config.getMaxWaitingCommits()).build();
        downloadWaitingCommits =
                ExpiringMap.builder().expiration(config.getWaitingCommitExpires(), TimeUnit.SECONDS)
                        .maxSize(config.getMaxWaitingCommits()).build();
    }

    @PostMapping("/file/upload/sign")
    public UploadSignResult uploadSign(HttpServletRequest request) {
        String              uploadId = ShortUUID.randomUUID();
        Map data     = New.hashMap("uploadId", uploadId);
        String              sign     = signer.sign(data, config.getSignExpires());
        String              url      = "file/upload?sign=" + sign;
        return new UploadSignResult(uploadId, url);
    }

    @PutMapping("/file/upload")
    @AllowAnonymous
    public ResponseEntity uploadFile(@Valid @RequestParam String sign, HttpServletRequest request) throws IOException {
        String uploadId;
        try {
            uploadId = (String) signer.verify(sign).get("uploadId");
        } catch (TokenVerifyException e) {
            log.info("Upload sign verify failed", e);
            return ResponseEntity.badRequest().body("Invalid upload sign, " + e.getMessage());
        }

        //{dir}/yyyyMMdd/HH/{fileId}
        String yyyyMMddHH = yyyyMMddHH();
        Path   dir        = config.getDirPath().resolve(yyyyMMddHH);
        if (!Files.exists(dir)) {
            Files.createDirectories(dir);
        }

        //try 10 times
        String fileId     = null;
        Path   pathToSave = null;
        for (int i = 0; i < 10; i++) {
            fileId = ShortID.randomID();
            pathToSave = dir.resolve(fileId);
            if (!Files.exists(pathToSave)) {
                break;
            } else {
                fileId = null;
            }
        }
        if (null == fileId) {
            fileId = ShortUUID.randomUUID();
            pathToSave = dir.resolve("./" + fileId);
        }
        File fileToSave = pathToSave.toAbsolutePath().toFile();
        writeFile(request, fileToSave);

        FileInfo fileInfo = new FileInfo();
        fileInfo.setId(fileId);
        fileInfo.setPath(Strings.replace(pathToSave.toString().substring(config.getDirPath().toString().length() + 1), "\\", "/"));
        fileInfo.setContentLength(fileToSave.length());
        uploadWaitingCommits.put(uploadId, fileInfo);
        return ResponseEntity.ok().build();
    }

    @PostMapping("/file/upload/commit")
    public UploadCommitResult uploadCommit(@Valid @RequestBody UploadCommitParams params) throws Throwable {
        String uploadId = params.getUploadId();

        FileInfo fileInfo = uploadWaitingCommits.remove(uploadId);
        if (null == fileInfo) {
            throw new BadRequestException("Upload id '" + uploadId + "' invalid or expired");
        }

        UploadCommitResult result = new UploadCommitResult();
        result.setId(encodeFilePathToId(fileInfo.getPath()));
        result.setContentLength(fileInfo.getContentLength());
        return result;
    }

    @PostMapping("/file/download/sign")
    public DownloadSignResult downloadSign(HttpServletRequest request, @RequestBody @Valid DownloadParams params) {
        Map data = Beans.toMap(params);
        String              sign = signer.sign(data, null != params.getExpires() ? params.getExpires() : config.getSignExpires());
        String              url  = "file/download?sign=" + sign;
        return new DownloadSignResult(url);
    }

    @AllowAnonymous
    @GetMapping("/assets/**//{filename:.+}")
    public ResponseEntity assets(@RequestParam String sign, HttpServletRequest request) throws Throwable {
        Map data;
        DownloadParams      params;

        try {
            data = signer.verify(sign);
        } catch (TokenVerifyException e) {
            log.info("Download sign verify failed", e);
            throw new BadRequestException("Invalid download sign, " + e.getMessage());
        }
        params = Converts.convert(data, DownloadParams.class);

        String servletPath = request.getServletPath();
        String filePath = servletPath.substring(servletPath.indexOf(ASSETS) + ASSETS.length() + 1);

        File file = config.getDirPath().resolve(filePath).toFile();
        if (!file.exists()) {
            return ResponseEntity.notFound().build();
        }
        if (config.isFileLastAccessEnabled()) {
            updateLastAccess(file);
        }

        ResponseEntity.BodyBuilder res = ResponseEntity.ok();
        if (null != params.getResponseHeaderOverrides()) {
            params.getResponseHeaderOverrides().forEach((name, value) -> {
                res.header(name, value);
            });
        }
        res.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
                .contentLength(file.length())
                .lastModified(file.lastModified());
        return res.body(new InputStreamResource(new FileInputStream(file)));
    }

    @AllowAnonymous
    @GetMapping("/file/download")
    public ResponseEntity downloadFile(@RequestParam String sign) throws Throwable {
        Map data;
        DownloadParams      params;
        try {
            data = signer.verify(sign);
        } catch (TokenVerifyException e) {
            log.info("Download sign verify failed", e);
            throw new BadRequestException("Invalid download sign, " + e.getMessage());
        }
        params = Converts.convert(data, DownloadParams.class);

        String filePath = decodeIdToFilePath(params.getFileId());

        File file = config.getDirPath().resolve(filePath).toFile();
        if (!file.exists()) {
            return ResponseEntity.notFound().build();
        }
        if (config.isFileLastAccessEnabled()) {
            updateLastAccess(file);
        }

        ResponseEntity.BodyBuilder res = ResponseEntity.ok();
        if (null != params.getResponseHeaderOverrides()) {
            params.getResponseHeaderOverrides().forEach((name, value) -> {
                res.header(name, value);
            });
        }
        res.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
                .contentLength(file.length())
                .lastModified(file.lastModified());
        return res.body(new InputStreamResource(new FileInputStream(file)));
    }

    protected void writeFile(HttpServletRequest request, File file) throws IOException {
        InputStream is = request.getInputStream();
        try {
            ReadableByteChannel in = Channels.newChannel(is);
            try (FileOutputStream out = new FileOutputStream(file)) {
                out.getChannel().transferFrom(in, 0, Long.MAX_VALUE);
            }
        } finally {
            IO.close(is);
        }
    }

    protected void updateLastAccess(File f) {
        File lastAccessFile = new File(f.getAbsolutePath() + ".access");
        if (!lastAccessFile.exists()) {
            try (FileOutputStream out = new FileOutputStream(lastAccessFile)) {
                lastAccessFile.setLastModified(System.currentTimeMillis());
            } catch (Exception e) {
                log.error("touch file error:" + lastAccessFile.getAbsolutePath());
            }
        } else {
            try {
                lastAccessFile.setLastModified(System.currentTimeMillis());
            } catch (Exception e) {
                log.error("touch file error:" + lastAccessFile.getAbsolutePath());
            }
        }
    }

    protected String encodeFilePathToId(String path) throws Throwable {
        byte[] bytes = Strings.getBytesUtf8(path);
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) ~(bytes[i]);
        }
        return Base64.getEncoder().encodeToString(bytes);
    }

    protected String decodeIdToFilePath(String encoded) throws Throwable {
        byte[] bytes = Base64.getDecoder().decode(encoded);
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) ~(bytes[i]);
        }
        String path = Strings.newStringUtf8(bytes);
        if (Strings.count(path, '/') < 2) {
            throw new BadRequestException("Invalid file id");
        }
        return path;
    }

    protected static String yyyyMMdd() {
        LocalDate date = LocalDate.now();

        String MM = date.getMonthValue() < 10 ?
                "0" + String.valueOf(date.getMonthValue()) : String.valueOf(date.getMonthValue());

        String dd = date.getDayOfMonth() < 10 ?
                "0" + String.valueOf(date.getDayOfMonth()) : String.valueOf(date.getDayOfMonth());

        return String.valueOf(date.getYear()) + MM + dd;
    }

    protected static String yyyyMMddHH() {
        int    hour = LocalTime.now().getHour();
        String HH   = hour < 10 ? "0" + hour : String.valueOf(hour);
        return yyyyMMdd() + "/" + HH;
    }

    protected static final class FileInfo {
        protected String id;
        protected String path;
        protected long   contentLength;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getPath() {
            return path;
        }

        public void setPath(String path) {
            this.path = path;
        }

        public long getContentLength() {
            return contentLength;
        }

        public void setContentLength(long contentLength) {
            this.contentLength = contentLength;
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy