/*
* Copyright 2010 NCHOVY
*
* 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 org.krakenapps.http.internal;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.krakenapps.http.UploadCallback;
import org.krakenapps.http.FileUploadService;
import org.krakenapps.http.UploadToken;
import org.krakenapps.http.UploadedFile;
import org.krakenapps.http.internal.util.MimeTypes;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.osgi.service.prefs.PreferencesService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FileUploadServlet extends HttpServlet implements FileUploadService {
private static final long serialVersionUID = 1L;
private final Logger logger = LoggerFactory.getLogger(FileUploadServlet.class.getName());
private static final String BASE_PATH = "base_path";
// upload config node
private Preferences prefs;
private File baseDir;
private File tempDir;
private ConcurrentMap<String, AtomicInteger> counters;
private ConcurrentMap<String, UploadItem> items;
private ConcurrentMap<String, Collection<String>> downloadTokens;
public FileUploadServlet(BundleContext bc) {
ServiceReference ref = bc.getServiceReference(PreferencesService.class.getName());
if (ref == null)
throw new IllegalStateException("osgi preferences service is required");
PreferencesService prefsvc = (PreferencesService) bc.getService(ref);
prefs = prefsvc.getSystemPreferences().node("upload");
}
public void start() {
baseDir = new File(prefs.get(BASE_PATH,
new File(System.getProperty("kraken.data.dir"), "kraken-http/upload/").getAbsolutePath()));
tempDir = new File(prefs.get(BASE_PATH,
new File(System.getProperty("kraken.data.dir"), "kraken-http/upload/").getAbsolutePath()));
baseDir.mkdirs();
tempDir.mkdirs();
// load counters per space
counters = new ConcurrentHashMap<String, AtomicInteger>();
items = new ConcurrentHashMap<String, UploadItem>();
downloadTokens = new ConcurrentHashMap<String, Collection<String>>();
try {
for (String spaceId : prefs.childrenNames()) {
Preferences node = prefs.node(spaceId);
int max = 0;
for (String resourceId : node.childrenNames()) {
int id = Integer.parseInt(resourceId);
if (id > max)
max = id;
}
counters.put(spaceId, new AtomicInteger(max));
}
} catch (BackingStoreException e) {
logger.warn("kraken http: failed to load upload space configurations", e);
}
}
public void stop() {
// nothing to do here
}
@Override
public File getBaseDirectory() {
return baseDir;
}
@Override
public void setBaseDirectory(File dir) {
if (dir == null)
throw new IllegalArgumentException("upload dir must be not null");
dir.mkdirs();
prefs.put(BASE_PATH, dir.getAbsolutePath());
baseDir = dir;
}
@Override
public Collection<UploadedFile> getFiles(String spaceId) {
Collection<UploadedFile> files = new ArrayList<UploadedFile>();
try {
if (!prefs.nodeExists(spaceId))
return files;
Preferences space = prefs.node(spaceId);
for (String resourceId : space.childrenNames()) {
Preferences node = space.node(resourceId);
int id = Integer.parseInt(resourceId);
String fileName = node.get("filename", null);
long fileSize = node.getLong("filesize", 0);
File file = new File(node.get("filepath", ""));
files.add(new UploadedFileImpl(id, fileName, fileSize, file));
}
return files;
} catch (BackingStoreException e) {
logger.warn("kraken http: cannot retrieve file names of space", e);
}
return null;
}
@Override
public int setUploadToken(UploadToken token, UploadCallback callback) {
if (token == null)
throw new IllegalArgumentException("upload token must be not null");
String spaceId = token.getSpaceId();
// add new counter if space does not exist
counters.putIfAbsent(spaceId, new AtomicInteger(0));
// get new resource id
AtomicInteger counter = counters.get(spaceId);
int nextId = counter.incrementAndGet();
// set upload item on waiting table
UploadItem old = items.putIfAbsent(token.getToken(), new UploadItem(token, nextId, callback));
if (old != null)
throw new IllegalStateException("duplicated http upload token: " + token.getToken());
return nextId;
}
@Override
public void removeDownloadToken(String token) {
if (token != null)
downloadTokens.remove(token);
}
@Override
public void setDownloadToken(String token, Collection<String> spaces) {
Collection<String> old = downloadTokens.putIfAbsent(token, spaces);
if (old != null)
throw new IllegalStateException("duplicated http download token: " + token);
}
@Override
public void writeFile(String token, InputStream is) throws IOException {
FileOutputStream os = null;
long totalReadBytes = 0;
// remove token from waiting table
UploadItem item = items.remove(token);
if (item == null)
throw new IOException("upload token not found: " + token);
logger.trace("kraken-http: new upload post for token {}, resouce id {}", token, item.resourceId);
try {
File temp = File.createTempFile("krakenhttp-", null, tempDir);
os = new FileOutputStream(temp);
byte[] buffer = new byte[8096];
while (true) {
int readBytes = is.read(buffer);
if (readBytes <= 0)
break;
totalReadBytes += readBytes;
os.write(buffer, 0, readBytes);
}
os.close();
os = null;
// check file size
if (totalReadBytes == item.token.getFileSize()) {
saveFile(temp, item);
} else {
// delete
temp.delete();
// failure callback
try {
item.callback.onUploadFile(item.token, null);
logger.info("kraken http: upload failure {} {}", item.token.getSpaceId(), item.token.getFileName());
} catch (Exception e) {
logger.warn("kraken http: upload callback should not throw any exception", e);
}
}
} catch (IOException e) {
throw e;
}
}
private void saveFile(File temp, UploadItem item) throws IOException {
// callback
UploadToken token = item.token;
try {
// build path
File spaceDir = new File(baseDir, token.getSpaceId());
spaceDir.mkdirs();
// move file
File dest = new File(spaceDir, Integer.toString(item.resourceId));
logger.trace("kraken-http: move {} to {}", temp.getAbsolutePath(), dest.getAbsolutePath());
if (!temp.renameTo(dest)) {
throw new IOException(String.format("kraken-http: move [%s] to [%s] failed", temp.getAbsolutePath(),
dest.getAbsolutePath()));
}
// save properties
Preferences space = prefs.node(token.getSpaceId());
Preferences node = space.node(Integer.toString(item.resourceId));
node.put("filename", token.getFileName());
node.putLong("filesize", token.getFileSize());
node.put("filepath", dest.getAbsolutePath());
space.flush();
space.sync();
// fire callbacks
if (item.callback != null)
item.callback.onUploadFile(token, item.resourceId);
} catch (BackingStoreException e) {
logger.warn("kraken-http: cannot save upload properties", e);
}
logger.trace("kraken-http: user file uploaded, space {}, name {}", token.getSpaceId(), token.getFileName());
}
@Override
public UploadedFile getFile(String token, String spaceId, int resourceId) throws IOException {
try {
Collection<String> spaces = downloadTokens.get(token);
if (spaces == null)
throw new IllegalStateException("download token not found: " + token);
if (!spaces.contains(spaceId))
throw new SecurityException("access denied for token [" + token + "], space [" + spaceId + "]");
if (!prefs.nodeExists(spaceId))
throw new IllegalStateException("space not found: " + spaceId);
Preferences space = prefs.node(spaceId);
String id = Integer.toString(resourceId);
if (!space.nodeExists(id))
throw new IllegalStateException("resource not found: " + resourceId);
Preferences p = space.node(id);
String fileName = p.get("filename", null);
long fileSize = p.getLong("filesize", 0);
File file = new File(p.get("filepath", null));
if (!file.exists())
throw new IOException("file not found: id " + resourceId + ", path: " + file.getAbsolutePath());
return new UploadedFileImpl(resourceId, fileName, fileSize, file);
} catch (BackingStoreException e) {
throw new IOException("cannot open upload metadata", e);
}
}
@Override
public void deleteFile(String spaceId, int resourceId) {
try {
// check and remove file metadata
String id = Integer.toString(resourceId);
if (!prefs.nodeExists(spaceId))
throw new IllegalStateException("space not found: " + spaceId);
Preferences space = prefs.node(spaceId);
if (!space.nodeExists(id))
throw new IllegalStateException("resource not found: " + resourceId);
Preferences p = space.node(id);
p.removeNode();
// sync
space.flush();
space.sync();
// remove physical file
File spaceDir = new File(baseDir, spaceId);
spaceDir.mkdirs();
File f = new File(spaceDir, Integer.toString(resourceId));
if (!f.delete())
throw new IllegalStateException(String.format(
"failed to delete uploaded file: space %s, resource %d, path %s", spaceId, resourceId,
f.getAbsolutePath()));
} catch (BackingStoreException e) {
logger.error("kraken-http: delete file failed", e);
}
}
private static class UploadItem {
public UploadToken token;
public int resourceId;
public UploadCallback callback;
public UploadItem(UploadToken token, int resourceId, UploadCallback callback) {
this.token = token;
this.resourceId = resourceId;
this.callback = callback;
}
}
@Override
public void log(String message, Throwable t) {
logger.warn("kraken http: file upload error", t);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
UploadedFile f = null;
FileInputStream is = null;
ServletOutputStream os = null;
String token = null;
String spaceId = null;
int resourceId = 0;
try {
token = getDownloadToken(req);
spaceId = req.getParameter("space");
resourceId = Integer.parseInt(req.getParameter("resource"));
if (token == null)
throw new IllegalStateException("download token not found");
f = getFile(token, spaceId, resourceId);
is = new FileInputStream(f.getFile());
os = resp.getOutputStream();
logger.trace("kraken http: open downstream for {}", f.getFile().getAbsolutePath());
String mimeType = MimeTypes.instance().getByFile(f.getFileName());
resp.setHeader("Content-Type", mimeType);
String dispositionType = null;
if (req.getParameter("force_download") != null)
dispositionType = "attachment";
else
dispositionType = "inline";
resp.setHeader("Content-Disposition", dispositionType + "; filename=\"" + f.getFileName() + "\"");
resp.setStatus(200);
resp.setContentLength((int) f.getFileSize());
byte[] b = new byte[8096];
while (true) {
int readBytes = is.read(b);
if (readBytes <= 0)
break;
os.write(b, 0, readBytes);
}
} catch (Exception e) {
resp.setStatus(500);
logger.warn("kraken-http: cannot download space " + spaceId + ", id " + resourceId);
} finally {
if (is != null)
try {
is.close();
} catch (IOException e) {
}
if (os != null)
try {
os.close();
} catch (IOException e) {
}
}
}
private String getDownloadToken(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
logger.trace("kraken-http: checking all cookie for download, {} = {}", cookie.getName(),
cookie.getValue());
if (cookie.getName().equals("kraken_session"))
return cookie.getValue();
}
}
return req.getParameter("session");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
String token = req.getHeader("X-Upload-Token");
if (token == null) {
logger.warn("kraken-http: upload token header not found for [{}:{}] stream", req.getRemoteAddr(),
req.getRemotePort());
return;
}
InputStream is = null;
try {
is = req.getInputStream();
writeFile(token, is);
resp.setStatus(200);
} catch (Exception e) {
resp.setStatus(500);
logger.warn("kraken-http: upload post failed", e);
} finally {
if (is == null)
return;
try {
is.close();
} catch (IOException e) {
}
}
}
public static class UploadedFileImpl implements UploadedFile {
private int resourceId;
private String fileName;
private long fileSize;
private File file;
public UploadedFileImpl(int resourceId, String fileName, long fileSize, File file) {
this.resourceId = resourceId;
this.fileName = fileName;
this.fileSize = fileSize;
this.file = file;
}
@Override
public int getResourceId() {
return resourceId;
}
@Override
public String getFileName() {
return fileName;
}
@Override
public long getFileSize() {
return fileSize;
}
@Override
public File getFile() {
return file;
}
@Override
public String toString() {
return String.format("resource id [%d], filename [%s], size [%d], path [%s]", resourceId, fileName,
fileSize, file.getAbsolutePath());
}
}
}