TmpFileMgr.java
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF 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 org.apache.doris.httpv2.util;
import org.apache.doris.common.util.Util;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
* Manager the file uploaded.
* This file manager is currently only used to manage files
* uploaded through the Upload RESTFul API.
* And limit the number and size of the maximum upload file.
* It can also browse or delete files through the RESTFul API.
*/
public class TmpFileMgr {
public static final Logger LOG = LogManager.getLogger(TmpFileMgr.class);
private static final long MAX_TOTAL_FILE_SIZE_BYTES = 1 * 1024 * 1024 * 1024L; // 1GB
private static final long MAX_TOTAL_FILE_NUM = 100;
public static final long MAX_SINGLE_FILE_SIZE = 100 * 1024 * 1024L; // 100MB
private static final String UPLOAD_DIR = "_doris_upload";
private AtomicLong fileIdGenerator = new AtomicLong(0);
private String rootDir;
private Map<Long, TmpFile> fileMap = Maps.newConcurrentMap();
private long totalFileSize = 0;
public TmpFileMgr(String dir) {
this.rootDir = dir + "/" + UPLOAD_DIR;
init();
}
private void init() {
File root = new File(rootDir);
if (!root.exists()) {
root.mkdirs();
} else if (!root.isDirectory()) {
throw new IllegalStateException("Path " + rootDir + " is not directory");
}
// delete all files under this dir at startup.
// This means that all uploaded files will be lost after FE restarts.
// This is just for simplicity.
Util.deleteDirectory(root);
root.mkdirs();
}
/**
* Simply used `synchronized` to allow only one user upload file at one time.
* So that we can easily control the number of files and total size of files.
*
* @param uploadFile
* @return
* @throws TmpFileException
*/
public synchronized TmpFile upload(UploadFile uploadFile) throws TmpFileException {
if (uploadFile.file.getSize() > MAX_SINGLE_FILE_SIZE) {
throw new TmpFileException("File size " + uploadFile.file.getSize()
+ " exceed limit " + MAX_SINGLE_FILE_SIZE);
}
if (totalFileSize + uploadFile.file.getSize() > MAX_TOTAL_FILE_SIZE_BYTES) {
throw new TmpFileException("Total file size will exceed limit " + MAX_TOTAL_FILE_SIZE_BYTES);
}
if (fileMap.size() > MAX_TOTAL_FILE_NUM) {
throw new TmpFileException("Number of temp file " + fileMap.size() + " exceed limit " + MAX_TOTAL_FILE_NUM);
}
long fileId = fileIdGenerator.incrementAndGet();
String fileUUID = UUID.randomUUID().toString();
TmpFile tmpFile = new TmpFile(fileId, fileUUID, uploadFile.file.getOriginalFilename(),
uploadFile.file.getSize(), uploadFile.columnSeparator);
try {
tmpFile.save(uploadFile.file);
} catch (IOException e) {
throw new TmpFileException("Failed to upload file. Reason: " + e.getMessage());
}
fileMap.put(tmpFile.id, tmpFile);
totalFileSize += uploadFile.file.getSize();
return tmpFile;
}
public TmpFile getFile(long id, String uuid) throws TmpFileException {
TmpFile tmpFile = fileMap.get(id);
if (tmpFile == null || !tmpFile.uuid.equals(uuid)) {
throw new TmpFileException("File with [" + id + "-" + uuid + "] does not exist");
}
return tmpFile;
}
public List<TmpFileBrief> listFiles() {
return fileMap.values().stream().map(t -> new TmpFileBrief(t)).collect(Collectors.toList());
}
/**
* Delete the specified file and remove it from fileMap
* @param fileId
* @param fileUUID
*/
public void deleteFile(Long fileId, String fileUUID) {
Iterator<Map.Entry<Long, TmpFile>> iterator = fileMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, TmpFile> entry = iterator.next();
if (entry.getValue().id == fileId && entry.getValue().uuid.equals(fileUUID)) {
entry.getValue().delete();
iterator.remove();
}
}
return;
}
public class TmpFile {
public final long id;
public final String uuid;
public final String originFileName;
public final long fileSize;
public String columnSeparator;
public String absPath;
public List<List<String>> lines = null;
public int maxColNum = 0;
private static final int MAX_PREVIEW_LINES = 10;
public TmpFile(long id, String uuid, String originFileName, long fileSize, String columnSeparator) {
this.id = id;
this.uuid = uuid;
this.originFileName = originFileName;
this.fileSize = fileSize;
this.columnSeparator = columnSeparator;
}
public void save(MultipartFile file) throws IOException {
File dest = new File(Joiner.on("/").join(rootDir, uuid));
boolean uploadSucceed = false;
try {
file.transferTo(dest);
this.absPath = dest.getAbsolutePath();
uploadSucceed = true;
LOG.info("upload file {} succeed at {}", this, dest.getAbsolutePath());
} catch (IOException e) {
LOG.warn("failed to upload file {}, dest: {}", this, dest.getAbsolutePath(), e);
throw e;
} finally {
if (!uploadSucceed) {
dest.delete();
}
}
}
public void setPreview() throws IOException {
lines = Lists.newArrayList();
String escapedColSep = Util.escapeSingleRegex(columnSeparator);
try (FileReader fr = new FileReader(absPath);
BufferedReader bf = new BufferedReader(fr)) {
String str;
while ((str = bf.readLine()) != null) {
String[] cols = str.split(escapedColSep, -1); // -1 to keep the last empty column
lines.add(Lists.newArrayList(cols));
if (cols.length > maxColNum) {
maxColNum = cols.length;
}
if (lines.size() >= MAX_PREVIEW_LINES) {
break;
}
}
}
}
// make a copy without lines and maxColNum.
// so that can call `setPreview` and will not affect other instance
public TmpFile copy() {
TmpFile copiedFile = new TmpFile(this.id, this.uuid, this.originFileName,
this.fileSize, this.columnSeparator);
copiedFile.absPath = this.absPath;
return copiedFile;
}
public void delete() {
File file = new File(absPath);
file.delete();
LOG.info("delete tmp file: {}", this);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[id=").append(id).append(", uuid=").append(uuid).append(", origin name=").append(originFileName)
.append(", size=").append(fileSize).append("]");
return sb.toString();
}
}
// a brief of TmpFile.
// TODO(cmy): it can be removed by using Lombok's annotation in TmpFile class
public static class TmpFileBrief {
public long id;
public String uuid;
public String originFileName;
public long fileSize;
public String columnSeparator;
public TmpFileBrief(TmpFile tmpFile) {
this.id = tmpFile.id;
this.uuid = tmpFile.uuid;
this.originFileName = tmpFile.originFileName;
this.fileSize = tmpFile.fileSize;
this.columnSeparator = tmpFile.columnSeparator;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getOriginFileName() {
return originFileName;
}
public void setOriginFileName(String originFileName) {
this.originFileName = originFileName;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public String getColumnSeparator() {
return columnSeparator;
}
public void setColumnSeparator(String columnSeparator) {
this.columnSeparator = columnSeparator;
}
}
public static class UploadFile {
public MultipartFile file;
public String columnSeparator;
public UploadFile(MultipartFile file, String columnSeparator) {
this.file = file;
this.columnSeparator = columnSeparator;
}
}
public static class TmpFileException extends Exception {
public TmpFileException(String msg) {
super(msg);
}
public TmpFileException(String msg, Throwable t) {
super(msg, t);
}
}
}