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