/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2014 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.plugins.flickr;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import org.syncany.config.Config;
import org.syncany.database.MultiChunkEntry.MultiChunkId;
import org.syncany.plugins.transfer.AbstractTransferManager;
import org.syncany.plugins.transfer.StorageException;
import org.syncany.plugins.transfer.files.MultichunkRemoteFile;
import org.syncany.plugins.transfer.files.RemoteFile;
import org.syncany.plugins.transfer.files.SyncanyRemoteFile;
import org.syncany.plugins.transfer.files.TempRemoteFile;
import com.flickr4java.flickr.Flickr;
import com.flickr4java.flickr.FlickrException;
import com.flickr4java.flickr.REST;
import com.flickr4java.flickr.RequestContext;
import com.flickr4java.flickr.auth.Auth;
import com.flickr4java.flickr.photos.Photo;
import com.flickr4java.flickr.photos.PhotoList;
import com.flickr4java.flickr.photos.Size;
import com.flickr4java.flickr.photosets.Photoset;
import com.flickr4java.flickr.uploader.UploadMetaData;
import com.flickr4java.flickr.uploader.Uploader;
public class FlickrTransferManager extends AbstractTransferManager {
private static final Logger logger = Logger.getLogger(FlickrTransferManager.class.getSimpleName());
private static final int FLICKR_MIN_IMAGE_BYTES = 17*17*3; // < 16x16 PNGs are rejected sometimes!
private Flickr flickr;
private Auth auth;
private String photosetId;
private Map<RemoteFile, Photo> remoteFilePhotoIdCache;
public FlickrTransferManager(FlickrTransferSettings settings, Config config) throws Exception {
super(settings, config);
this.flickr = new Flickr(FlickrTransferPlugin.APP_KEY, FlickrTransferPlugin.APP_SECRET, new REST());
this.auth = settings.getAuth().toAuth();
this.photosetId = settings.getAlbum();
this.remoteFilePhotoIdCache = new HashMap<RemoteFile, Photo>();
// Init Flickr object
flickr.setAuth(auth);
RequestContext.getRequestContext().setAuth(auth);
Flickr.debugRequest = false;
Flickr.debugStream = false;
}
public FlickrTransferSettings getSettings() {
return (FlickrTransferSettings) settings;
}
@Override
public void connect() throws StorageException {
// Nothing
}
@Override
public void disconnect() {
// Nothing
}
@Override
public void init(boolean createIfRequired) throws StorageException {
if (createIfRequired) {
if (photosetId == null) {
logger.log(Level.INFO, "Flickr Init: Create target enabled, and NO album ID given. Creating album ...");
photosetId = createNewAlbum();
getSettings().setAlbum(photosetId);
}
else {
logger.log(Level.INFO, "Flickr Init: Create target enabled, but album ID given (" + photosetId + "). Using this album. Nothing to do.");
}
}
else {
if (photosetId == null) {
logger.log(Level.INFO, "Flickr Init: Create target NOT enabled, and NO album ID given. Cannot continue.");
throw new StorageException("Album ID required if 'create target' option not selected.");
}
else {
logger.log(Level.INFO, "Flickr Init: Create target NOT enabled, album ID given (" + photosetId + "). Using this album. Nothing to do.");
}
}
}
private String createNewAlbum() throws StorageException {
try {
// Create and upload dummy file (album needs at least one photo)
Path dummyFileTempPath = Files.createTempFile("syncany-temp", ".tmp");
Files.write(dummyFileTempPath, "Syncany rocks!".getBytes());
String dummyPhotoId = upload(dummyFileTempPath.toFile(), new TempRemoteFile(new MultichunkRemoteFile(MultiChunkId.secureRandomMultiChunkId())), false);
Files.delete(dummyFileTempPath);
// Create album
String title = "Syncany " + (1000 + Math.abs(new Random().nextInt(8999)));
String description = "Flickr-based Syncany repository. Details at www.syncany.org!";
Photoset photoset = flickr.getPhotosetsInterface().create(title, description, dummyPhotoId);
return photoset.getId();
}
catch (Exception e) {
throw new StorageException("Cannot initialize repository. Creating Flickr album failed.", e);
}
}
@Override
public void download(RemoteFile remoteFile, File localFile) throws StorageException {
Photo photo = getPhoto(remoteFile);
try {
// Copy PNG file to local cache; This indirection is necessary, because there are some
// ZIP/stream issues when the input stream is directly handed to the PNG decoder.
InputStream rawImageStream = flickr.getPhotosInterface().getImageAsStream(photo, Size.ORIGINAL);
File tmpFile = createTempFile(remoteFile.getName());
FileUtils.copyInputStreamToFile(rawImageStream, tmpFile);
rawImageStream.close();
// Decode PNG from file to byte array and write to final file (removes Flickr-bug padding)
byte[] paddedPngData = PngEncoder.decodeFromPng(tmpFile);
FileOutputStream localFileOutputStream = new FileOutputStream(localFile);
localFileOutputStream.write(paddedPngData, FLICKR_MIN_IMAGE_BYTES, paddedPngData.length-FLICKR_MIN_IMAGE_BYTES);
localFileOutputStream.close();
}
catch (Exception e) {
throw new StorageException("Cannot download image " + remoteFile + ", Flickr photo ID " + photo.getId(), e);
}
}
@Override
public void upload(File localFile, RemoteFile remoteFile) throws StorageException {
upload(localFile, remoteFile, true);
}
private String upload(File localFile, RemoteFile remoteFile, boolean addToPhotoset) throws StorageException {
try {
UploadMetaData metaData = new UploadMetaData();
metaData.setFilename(remoteFile.getName() + ".png");
metaData.setTitle(remoteFile.getName());
metaData.setFilemimetype("image/png");
// Some weird Flickr bug: Images with dimensions < 17x17 are sometimes rejected.
ByteArrayOutputStream paddedContentsOutputStream = new ByteArrayOutputStream();
paddedContentsOutputStream.write(new byte[FLICKR_MIN_IMAGE_BYTES]);
paddedContentsOutputStream.write(FileUtils.readFileToByteArray(localFile));
byte[] fileContents = paddedContentsOutputStream.toByteArray();
// Encode local file to PNG image
ByteArrayOutputStream encodedPngOutputStream = new ByteArrayOutputStream();
PngEncoder.encodeToPng(fileContents, encodedPngOutputStream);
encodedPngOutputStream.close();
byte[] pngEncodedFileContents = encodedPngOutputStream.toByteArray();
// Upload PNG image to Flickr
Uploader uploader = flickr.getUploader();
String photoId = uploader.upload(pngEncodedFileContents, metaData);
// Add image to photoset (album)
if (addToPhotoset) {
flickr.getPhotosetsInterface().addPhoto(photosetId, photoId);
}
logger.log(Level.INFO, "Uploaded file " + localFile + " to " + remoteFile + ", as photo ID " + photoId);
return photoId;
}
catch (Exception e) {
throw new StorageException("Cannot upload file " + localFile + " to remote file ", e);
}
}
@Override
public boolean delete(RemoteFile remoteFile) throws StorageException {
try {
Photo photo = getPhoto(remoteFile);
flickr.getPhotosInterface().delete(photo.getId());
return true;
}
catch (Exception e) {
logger.log(Level.WARNING, "Cannot delete remote file " + remoteFile + ". IGNORING.", e);
return false;
}
}
@Override
public void move(RemoteFile sourceFile, RemoteFile targetFile) throws StorageException {
try {
Photo photo = getPhoto(sourceFile);
flickr.getPhotosInterface().setMeta(photo.getId(), targetFile.getName(), null);
}
catch (Exception e) {
throw new StorageException(e);
}
}
@Override
public <T extends RemoteFile> Map<String, T> list(Class<T> remoteFileClass) throws StorageException {
try {
Map<String, T> fileList = new HashMap<String, T>();
boolean morePhotos = true;
int maxPhotos = 1000;
int currentPage = 1;
while (morePhotos) {
PhotoList<Photo> partialPhotoList = flickr.getPhotosetsInterface().getPhotos(photosetId, maxPhotos, currentPage);
for (Photo photo : partialPhotoList) {
try {
RemoteFile remoteFile = RemoteFile.createRemoteFile(photo.getTitle());
if (remoteFile.getClass().equals(remoteFileClass)) {
T concreteRemoteFile = remoteFileClass.cast(remoteFile);
fileList.put(remoteFile.getName(), concreteRemoteFile);
}
remoteFilePhotoIdCache.put(remoteFile, photo);
}
catch (Exception e) {
// Ignore invalid filenames
}
}
if (partialPhotoList.size() < 1000) {
morePhotos = false;
}
else {
currentPage++;
morePhotos = true;
}
}
return fileList;
}
catch (FlickrException e) {
throw new StorageException(e);
}
}
@Override
public boolean testTargetCanWrite() {
return true;
}
@Override
public boolean testTargetExists() {
try {
if (photosetId == null) {
return false;
}
else {
Photoset photoset = flickr.getPhotosetsInterface().getInfo(photosetId);
return photoset != null;
}
}
catch (FlickrException e) {
logger.log(Level.SEVERE, "Cannot get information about photoset.", e);
return false;
}
}
@Override
public boolean testTargetCanCreate() {
return true;
}
@Override
public boolean testRepoFileExists() {
try {
return list(SyncanyRemoteFile.class).size() == 1;
}
catch (StorageException e) {
logger.log(Level.SEVERE, "Cannot get information about repo file.", e);
return false;
}
}
private Photo getPhoto(RemoteFile remoteFile) throws StorageException {
Photo photo = remoteFilePhotoIdCache.get(remoteFile);
if (photo != null) {
return photo;
}
else {
list(remoteFile.getClass()); // Update cache!
photo = remoteFilePhotoIdCache.get(remoteFile);
if (photo != null) {
return photo;
}
else {
throw new StorageException("Cannot find remote file " + remoteFile);
}
}
}
}