/* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.capedwarf.files; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import com.google.appengine.api.NamespaceManager; import com.google.appengine.api.blobstore.BlobInfo; import com.google.appengine.api.blobstore.BlobInfoFactory; import com.google.appengine.api.blobstore.BlobKey; import com.google.appengine.api.blobstore.FileInfo; import com.google.appengine.api.datastore.Blob; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.EntityNotFoundException; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.files.AppEngineFile; import com.google.appengine.api.files.FileReadChannel; import com.google.appengine.api.files.FileStat; import com.google.appengine.api.files.FileWriteChannel; import com.google.appengine.api.files.FinalizationException; import com.google.appengine.api.files.GSFileOptions; import com.google.appengine.api.files.RecordReadChannel; import com.google.appengine.api.files.RecordWriteChannel; import org.infinispan.io.GridFilesystem; import org.jboss.capedwarf.common.app.Application; import org.jboss.capedwarf.common.infinispan.InfinispanUtils; import org.jboss.capedwarf.common.io.DigestResult; import org.jboss.capedwarf.shared.reflection.ReflectionUtils; /** * JBoss GAE File service. * * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a> * @author <a href="mailto:marko.luksa@gmail.com">Marko Luksa</a> */ @SuppressWarnings("deprecation") public class CapedwarfFileService implements ExposedFileService { private static final String DEFAULT_MIME_TYPE = "application/octet-stream"; private static final String KIND_TEMP_BLOB_INFO = "__BlobInfo_temp__"; private static final String MD5_HASH_STATE = BlobInfoFactory.MD5_HASH + "__State"; private DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService(); private BlobInfoFactory blobInfoFactory; private synchronized BlobInfoFactory getBlobInfoFactory() { if (blobInfoFactory == null) { blobInfoFactory = new BlobInfoFactory(datastoreService); } return blobInfoFactory; } public BlobInfo getBlobInfo(BlobKey key) { return getBlobInfoFactory().loadBlobInfo(key); } public FileInfo getFileInfo(BlobKey key) { Entity info = getFileInfo(getFileInfoKey(key)); if (info != null) { String contentType = (String) info.getProperty(BlobInfoFactory.CONTENT_TYPE); Date creation = (Date) info.getProperty(BlobInfoFactory.CREATION); String filename = (String) info.getProperty(BlobInfoFactory.FILENAME); Object sp = info.getProperty(BlobInfoFactory.SIZE); long size = (sp != null ? (Long) sp : -1); String md5Hash = (String) info.getProperty(BlobInfoFactory.MD5_HASH); // gsObjectName?? String keyString = key.getKeyString(); int p = keyString.lastIndexOf("/"); String gsObjectName = keyString.substring(p + 1); return new FileInfo(contentType, creation, filename, size, md5Hash, gsObjectName); } else { return null; } } public AppEngineFile createNewBlobFile(String mimeType) throws IOException { return createNewBlobFile(mimeType, ""); } public AppEngineFile createNewBlobFile(String contentType, String uploadedFileName) throws IOException { return createNewBlobFile(contentType, null, uploadedFileName, AppEngineFile.FileSystem.BLOBSTORE); } public AppEngineFile createNewGSFile(GSFileOptions options) throws IOException { final GSFileOptionsAdapter adapter = new GSFileOptionsAdapter(options); final String fileName = adapter.getFileName(); String uploadedFilename = adapter.getUserMetadata("uploaded-filename"); if (uploadedFilename == null) { uploadedFilename = fileName; } return createNewBlobFile(adapter.getMimeType(), fileName, null, uploadedFilename, AppEngineFile.FileSystem.GS); } public void delete(BlobKey... blobKeys) { GridFilesystem gfs = getGridFilesystem(); for (BlobKey key : blobKeys) { File file = gfs.getFile(getFilePath(key)); //noinspection ResultOfMethodCallIgnored file.delete(); } } public InputStream getStream(BlobKey blobKey) throws FileNotFoundException { GridFilesystem gfs = getGridFilesystem(); return gfs.getInput(getFilePath(blobKey)); } public FileWriteChannel openWriteChannel(AppEngineFile file, boolean lock) throws IOException { if (isFinalized(file)) { throwFinalizationException(); } createBlobstoreDirIfNeeded(file); GridFilesystem gfs = getGridFilesystem(); return new CapedwarfFileWriteChannel(file, gfs.getWritableChannel(getFilePath(file), true), this, lock); } public FileReadChannel openReadChannel(AppEngineFile file, boolean lock) throws IOException { if (!exists(file)) { throw new FileNotFoundException("File " + file + " not found."); } if (!isFinalized(file)) { throwFinalizationException(); } GridFilesystem gfs = getGridFilesystem(); return new CapedwarfFileReadChannel(gfs.getReadableChannel(getFilePath(file))); } public String getDefaultGsBucketName() { return String.format("%s.appspot.com", Application.getAppId()); // OK? } @Override public boolean exists(AppEngineFile file) { return getGfsFile(file).exists(); } public RecordWriteChannel openRecordWriteChannel(AppEngineFile file, boolean lock) throws IOException { return ReflectionUtils.newInstance( "com.google.appengine.api.files.RecordWriteChannelImpl", new Class[]{FileWriteChannel.class}, new Object[]{openWriteChannel(file, lock)}); } public RecordReadChannel openRecordReadChannel(AppEngineFile file, boolean lock) throws IOException { return ReflectionUtils.newInstance( "com.google.appengine.api.files.RecordReadChannelImpl", new Class[]{FileReadChannel.class}, new Object[]{openReadChannel(file, lock)}); } public BlobKey getBlobKey(AppEngineFile file) { AppEngineFileAdapter adapter = new AppEngineFileAdapter(file); return adapter.getBlobKey(); } public AppEngineFile getBlobFile(BlobKey blobKey) { AppEngineFile file = new AppEngineFile("/" + blobKey.getKeyString()); AppEngineFileAdapter.setCachedBlobKey(file, blobKey); return file; } public FileStat stat(AppEngineFile file) throws IOException { Entity info = getFileInfo(getFileInfoKey(getBlobKey(file))); if (info == null) { info = getFileInfo(getTempBlobInfoKey(file)); if (info == null) { throw new FileNotFoundException(file.toString()); } else { throw ReflectionUtils.newInstance(FinalizationException.class); } } else { final FileStat stat = new FileStat(); stat.setFinalized(true); stat.setFilename(file.getFullPath()); stat.setLength((Long) info.getProperty(BlobInfoFactory.SIZE)); // TODO -- setMtime, setCtime return stat; } } public void delete(AppEngineFile... appEngineFiles) throws IOException { final Set<AppEngineFile> failed = new HashSet<AppEngineFile>(); for (AppEngineFile aef : appEngineFiles) { if (getGfsFile(aef).delete() == false) { failed.add(aef); } } if (failed.isEmpty() == false) throw new IOException("Failed to delete files: " + failed); } private Key getTempBlobInfoKey(AppEngineFile file) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { return KeyFactory.createKey(KIND_TEMP_BLOB_INFO, file.getFullPath()); } finally { NamespaceManager.set(origNamespace); } } private Key getFileInfoKey(BlobKey key) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { return KeyFactory.createKey(BlobInfoFactory.KIND, key.getKeyString()); } finally { NamespaceManager.set(origNamespace); } } public AppEngineFile createNewBlobFile(String contentType, String bucketName, String uploadedFileName, AppEngineFile.FileSystem fs) throws IOException { return createNewBlobFile(contentType, null, bucketName, uploadedFileName, fs); } private AppEngineFile createNewBlobFile(String contentType, String fileName, String bucketName, String uploadedFilename, AppEngineFile.FileSystem fs) throws IOException { if (contentType == null || contentType.trim().isEmpty()) { contentType = DEFAULT_MIME_TYPE; } AppEngineFile file; if (fileName == null) { fileName = generateUniqueFileName(); if (bucketName != null) { fileName = String.format("%s/%s", bucketName, fileName); } file = new AppEngineFile(fs, fileName); } else { // this is already a proper full file name file = new AppEngineFile(fileName); } storeTemporaryBlobInfo(file, contentType, new Date(), uploadedFilename); return file; } private void storeTemporaryBlobInfo(AppEngineFile file, String contentType, Date creationTimestamp, String uploadedFilename) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { Entity tempBlobInfo = new Entity(getTempBlobInfoKey(file)); tempBlobInfo.setProperty(BlobInfoFactory.CONTENT_TYPE, contentType); tempBlobInfo.setProperty(BlobInfoFactory.CREATION, creationTimestamp); tempBlobInfo.setProperty(BlobInfoFactory.FILENAME, uploadedFilename); datastoreService.put(tempBlobInfo); } finally { NamespaceManager.set(origNamespace); } } private Entity getTemporaryInfo(AppEngineFile file) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { return datastoreService.get(getTempBlobInfoKey(file)); } catch (EntityNotFoundException e) { throw new IllegalStateException("Cannot finalize file " + file + ". Cannot find temp blob info."); } finally { NamespaceManager.set(origNamespace); } } void saveMd5(AppEngineFile file, DigestResult dg) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { Entity info = getTemporaryInfo(file); byte[] bytes = dg.getState(); if (bytes != null) { info.setProperty(MD5_HASH_STATE, new Blob(bytes)); } info.setProperty(BlobInfoFactory.MD5_HASH, dg.getDigest()); datastoreService.put(info); } finally { NamespaceManager.set(origNamespace); } } DigestResult readMd5(AppEngineFile file) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { Entity info = getTemporaryInfo(file); Blob state = (Blob) info.getProperty(MD5_HASH_STATE); String md5 = (String) info.getProperty(BlobInfoFactory.MD5_HASH); return new DigestResult(state != null ? state.getBytes() : null, md5); } finally { NamespaceManager.set(origNamespace); } } void finalizeFile(AppEngineFile file) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { Entity tempBlobInfo = getTemporaryInfo(file); File gfsFile = getGfsFile(file); if (gfsFile.exists() == false) { throw new IllegalStateException("Cannot finalize file " + file + ". Cannot find file on grid filesystem."); } String blobKeyString = getBlobKey(file).getKeyString(); Entity blobInfo = new Entity(BlobInfoFactory.KIND, blobKeyString); blobInfo.setProperty(BlobInfoFactory.CONTENT_TYPE, tempBlobInfo.getProperty(BlobInfoFactory.CONTENT_TYPE)); blobInfo.setProperty(BlobInfoFactory.CREATION, tempBlobInfo.getProperty(BlobInfoFactory.CREATION)); blobInfo.setProperty(BlobInfoFactory.FILENAME, tempBlobInfo.getProperty(BlobInfoFactory.FILENAME)); blobInfo.setProperty(BlobInfoFactory.MD5_HASH, tempBlobInfo.getProperty(BlobInfoFactory.MD5_HASH)); blobInfo.setProperty(BlobInfoFactory.SIZE, gfsFile.length()); datastoreService.put(blobInfo); } finally { NamespaceManager.set(origNamespace); } } private File getGfsFile(AppEngineFile file) { GridFilesystem gfs = getGridFilesystem(); return gfs.getFile(getFilePath(file)); } private String generateUniqueFileName() { return UUID.randomUUID().toString(); } private void throwFinalizationException() throws FinalizationException { throw ReflectionUtils.newInstance(FinalizationException.class); } private boolean isFinalized(AppEngineFile file) { BlobKey blobKey = getBlobKey(file); BlobInfo blobInfo = getBlobInfo(blobKey); return blobInfo != null; } private void createBlobstoreDirIfNeeded(AppEngineFile file) { // TODO: this is temporary String fullPath = file.getFullPath(); int p = fullPath.lastIndexOf("/"); String dirs = fullPath.substring(0, p); //noinspection ResultOfMethodCallIgnored getGridFilesystem().getFile(dirs).mkdirs(); } private String getFilePath(AppEngineFile file) { return AppEngineFileAdapter.getFilePath(file); } private String getFilePath(BlobKey blobKey) { return blobKey.getKeyString(); } private GridFilesystem getGridFilesystem() { return InfinispanUtils.getGridFilesystem(Application.getAppId()); } protected Entity getFileInfo(Key key) { final String origNamespace = NamespaceManager.get(); NamespaceManager.set(""); try { Map<Key, Entity> map = datastoreService.get(Collections.singleton(key)); return (map.isEmpty() ? null : map.values().iterator().next()); } finally { NamespaceManager.set(origNamespace); } } }