001/* 002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Andre Justo 016 */ 017package org.nuxeo.ecm.liveconnect.dropbox; 018 019import com.dropbox.core.DbxClient; 020import com.dropbox.core.DbxEntry; 021import com.dropbox.core.DbxException; 022import com.dropbox.core.DbxRequestConfig; 023import com.dropbox.core.DbxThumbnailFormat; 024import com.dropbox.core.DbxThumbnailSize; 025import com.google.api.client.auth.oauth2.Credential; 026import com.google.api.client.http.GenericUrl; 027import com.google.api.client.http.HttpRequestFactory; 028import com.google.api.client.http.HttpResponse; 029 030import org.apache.commons.lang3.StringUtils; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.ecm.core.api.Blob; 034import org.nuxeo.ecm.core.api.DocumentModel; 035import org.nuxeo.ecm.core.blob.BlobManager.BlobInfo; 036import org.nuxeo.ecm.core.blob.BlobManager.UsageHint; 037import org.nuxeo.ecm.core.blob.BlobProvider; 038import org.nuxeo.ecm.core.blob.ManagedBlob; 039import org.nuxeo.ecm.core.blob.SimpleManagedBlob; 040import org.nuxeo.ecm.core.cache.Cache; 041import org.nuxeo.ecm.core.cache.CacheService; 042import org.nuxeo.ecm.core.model.Document; 043import org.nuxeo.ecm.liveconnect.update.BatchUpdateBlobProvider; 044import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry; 045import org.nuxeo.ecm.platform.mimetype.service.MimetypeRegistryService; 046import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider; 047import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProviderRegistry; 048import org.nuxeo.runtime.api.Framework; 049 050import java.io.IOException; 051import java.io.InputStream; 052import java.net.URI; 053import java.net.URISyntaxException; 054import java.util.ArrayList; 055import java.util.Collections; 056import java.util.List; 057import java.util.Locale; 058import java.util.Map; 059 060import javax.servlet.http.HttpServletRequest; 061 062/** 063 * Provider for blobs getting information from Dropbox. 064 * 065 * @since 7.3 066 */ 067public class DropboxBlobProvider implements BlobProvider, BatchUpdateBlobProvider { 068 069 private static final Log log = LogFactory.getLog(DropboxBlobProvider.class); 070 071 public static final String PREFIX = "dropbox"; 072 073 private static final String APPLICATION_NAME = "Nuxeo/0"; 074 075 private static final String FILE_CACHE_NAME = "dropbox"; 076 077 private static final String DROPBOX_DOCUMENT_TO_BE_UPDATED_PP = "dropbox_document_to_be_updated"; 078 079 /** 080 * {@link DbxEntry.File} resource cache 081 */ 082 private Cache fileCache; 083 084 @Override 085 public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { 086 } 087 088 @Override 089 public void close() { 090 } 091 092 @Override 093 public Blob readBlob(BlobInfo blobInfo) throws IOException { 094 return new SimpleManagedBlob(blobInfo); 095 } 096 097 @Override 098 public boolean supportsWrite() { 099 return false; 100 } 101 102 @Override 103 public String writeBlob(Blob blob, Document doc) { 104 throw new UnsupportedOperationException("Writing a blob to Dropbox is not supported"); 105 } 106 107 @Override 108 public URI getURI(ManagedBlob blob, UsageHint usage, HttpServletRequest servletRequest) throws IOException { 109 String url = null; 110 String fileInfo = getFileInfo(blob.getKey()); 111 String user = getUser(fileInfo); 112 String filePath = getFilePath(fileInfo); 113 DbxClient client = getDropboxClient(getCredential(user)); 114 try { 115 switch (usage) { 116 case STREAM: 117 url = client.createTemporaryDirectUrl(filePath).url; 118 break; 119 case DOWNLOAD: 120 url = client.createShareableUrl(filePath); 121 url = url.replace("dl=0", "dl=1"); // enable download flag in url 122 break; 123 case VIEW: 124 url = client.createShareableUrl(filePath); 125 break; 126 } 127 } catch (DbxException e) { 128 throw new IOException("Failed to get Dropbox file URI " + e); 129 } 130 return url != null ? asURI(url) : null; 131 } 132 133 protected InputStream getStream(URI uri) throws IOException { 134 return doGet(uri); 135 } 136 137 @Override 138 public Map<String, URI> getAvailableConversions(ManagedBlob blob, UsageHint hint) throws IOException { 139 return Collections.emptyMap(); 140 } 141 142 @Override 143 public InputStream getThumbnail(ManagedBlob blob) throws IOException { 144 String fileInfo = getFileInfo(blob.getKey()); 145 String user = getUser(fileInfo); 146 String filePath = getFilePath(fileInfo); 147 try { 148 DbxClient.Downloader downloader = getDropboxClient(getCredential(user)).startGetThumbnail(DbxThumbnailSize.w64h64, 149 DbxThumbnailFormat.bestForFileName(filePath, DbxThumbnailFormat.JPEG), filePath, null); 150 151 if (downloader == null) { 152 return null; 153 } 154 return downloader.body; 155 } catch (DbxException e) { 156 throw new IOException("Failed to get Dropbox file thumbnail " + e); 157 } 158 } 159 160 @Override 161 public InputStream getStream(ManagedBlob blob) throws IOException { 162 URI uri = getURI(blob, UsageHint.STREAM, null); 163 return uri == null ? null : getStream(uri); 164 } 165 166 @Override 167 public InputStream getConvertedStream(ManagedBlob blob, String mimeType, DocumentModel doc) throws IOException { 168 Map<String, URI> conversions = getAvailableConversions(blob, UsageHint.STREAM); 169 URI uri = conversions.get(mimeType); 170 if (uri == null) { 171 return null; 172 } 173 return getStream(uri); 174 } 175 176 @Override 177 public ManagedBlob freezeVersion(ManagedBlob blob, Document doc) throws IOException { 178 return null; 179 } 180 181 /** 182 * Gets the blob for a Dropbox file. 183 * 184 * @param fileInfo the file info ({email}:{filePath}) 185 * @return the blob 186 */ 187 protected Blob getBlob(String fileInfo) throws IOException { 188 String user = getUser(fileInfo); 189 String filePath = getFilePath(fileInfo); 190 DbxEntry.File file = getFile(user, filePath); 191 String key = String.format("%s:%s:%s", PREFIX, user, filePath); 192 BlobInfo blobInfo = new BlobInfo(); 193 blobInfo.key = key; 194 blobInfo.filename = file.name; 195 blobInfo.length = file.numBytes; 196 blobInfo.mimeType = getMimetypeFromFilename(file.name); 197 blobInfo.encoding = null; 198 blobInfo.digest = file.rev; 199 return new SimpleManagedBlob(blobInfo); 200 } 201 202 /** 203 * Removes the prefix from the key. 204 */ 205 protected String getFileInfo(String key) { 206 int colon = key.indexOf(':'); 207 if (colon < 0) { 208 throw new IllegalArgumentException(key); 209 } 210 String fileInfo = key.substring(colon + 1); 211 return fileInfo; 212 } 213 214 protected String getUser(String fileInfo) { 215 return getFileInfoParts(fileInfo)[0]; 216 } 217 218 protected String getFilePath(String fileInfo) { 219 return getFileInfoParts(fileInfo)[1]; 220 } 221 222 protected String[] getFileInfoParts(String fileInfo) { 223 String[] parts = fileInfo.split(":"); 224 if (parts.length != 2) { 225 throw new IllegalArgumentException(fileInfo); 226 } 227 return parts; 228 } 229 230 protected Credential getCredential(String user) throws IOException { 231 return getCredentialFactory().build(user); 232 } 233 234 protected OAuthCredentialFactory getCredentialFactory() { 235 return new OAuthCredentialFactory(getOAuth2Provider()); 236 } 237 238 protected DbxClient getDropboxClient(Credential credential) throws IOException { 239 return getDropboxClient(credential.getAccessToken()); 240 } 241 242 protected DbxClient getDropboxClient(String accessToken) throws IOException { 243 DbxRequestConfig config = new DbxRequestConfig(APPLICATION_NAME, Locale.getDefault().toString()); 244 return new DbxClient(config, accessToken); 245 } 246 247 /** 248 * Retrieves and caches a {@link DbxEntry.File} resource. 249 * 250 * @return a {@link DbxEntry.File} resource. 251 */ 252 protected DbxEntry.File getFile(String user, String filePath) throws IOException { 253 DbxEntry.File fileResource = (DbxEntry.File) getFileCache().get(filePath); 254 if (fileResource == null) { 255 try { 256 DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath); 257 if (fileMetadata == null) { 258 return null; 259 } 260 fileResource = fileMetadata.asFile(); 261 getFileCache().put(filePath, fileResource); 262 } catch (DbxException e) { 263 throw new IOException("Failed to get Dropbox file metadata " + e); 264 } 265 } 266 return fileResource; 267 } 268 269 /** 270 * Executes a GET request 271 */ 272 protected InputStream doGet(URI url) throws IOException { 273 HttpRequestFactory requestFactory = getOAuth2Provider().getRequestFactory(); 274 HttpResponse response = requestFactory.buildGetRequest(new GenericUrl(url)).execute(); 275 return response.getContent(); 276 } 277 278 /** 279 * Parse a {@link URI}. 280 * 281 * @return the {@link URI} or null if it fails 282 */ 283 protected static URI asURI(String link) { 284 try { 285 return new URI(link); 286 } catch (URISyntaxException e) { 287 log.error("Invalid URI: " + link, e); 288 return null; 289 } 290 } 291 292 protected String getClientId() { 293 OAuth2ServiceProvider provider = getOAuth2Provider(); 294 return provider != null ? provider.getClientId() : null; 295 } 296 297 private Cache getFileCache() { 298 if (fileCache == null) { 299 fileCache = Framework.getService(CacheService.class).getCache(FILE_CACHE_NAME); 300 } 301 return fileCache; 302 } 303 304 protected DropboxOAuth2ServiceProvider getOAuth2Provider() { 305 return (DropboxOAuth2ServiceProvider) Framework.getLocalService( 306 OAuth2ServiceProviderRegistry.class).getProvider(PREFIX); 307 } 308 309 private String getMimetypeFromFilename(String filename) { 310 MimetypeRegistryService mimetypeRegistryService = (MimetypeRegistryService) Framework.getLocalService( 311 MimetypeRegistry.class); 312 return mimetypeRegistryService.getMimetypeFromFilename(filename); 313 } 314 315 @Override 316 public List<DocumentModel> checkChangesAndUpdateBlob(List<DocumentModel> docs) { 317 List<DocumentModel> changedDocuments = new ArrayList<>(); 318 for (DocumentModel doc : docs) { 319 final SimpleManagedBlob blob = (SimpleManagedBlob) doc.getProperty("content").getValue(); 320 if (blob == null) { 321 continue; 322 } 323 if (isVersion(blob)) { 324 continue; 325 } 326 String fileInfo = getFileInfo(blob.key); 327 String user = getUser(fileInfo); 328 String filePath = getFilePath(fileInfo); 329 try { 330 DbxEntry.File file = getFileNoCache(user, filePath); 331 if (StringUtils.isBlank(blob.getDigest()) || !blob.getDigest().equals(file.rev)) { 332 log.trace("Updating " + blob.key); 333 getFileCache().invalidate(filePath); 334 doc.setPropertyValue("content", (SimpleManagedBlob) getBlob(fileInfo)); 335 changedDocuments.add(doc); 336 } 337 338 } catch (DbxException | IOException e) { 339 log.error("Could not update dropbox document " + filePath, e); 340 } 341 } 342 return changedDocuments; 343 } 344 345 protected DbxEntry.File getFileNoCache(String user, String filePath) throws DbxException, IOException { 346 DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath); 347 if (fileMetadata == null) { 348 return null; 349 } 350 DbxEntry.File file = fileMetadata.asFile(); 351 return file; 352 } 353 354 @Override 355 public String getPageProviderNameForUpdate() { 356 return DROPBOX_DOCUMENT_TO_BE_UPDATED_PP; 357 } 358 359 @Override 360 public String getBlobPrefix() { 361 return PREFIX; 362 } 363 364}