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.AbstractBlobProvider; 036import org.nuxeo.ecm.core.blob.BlobManager.BlobInfo; 037import org.nuxeo.ecm.core.blob.BlobManager.UsageHint; 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 extends AbstractBlobProvider implements BatchUpdateBlobProvider { 068 069 private static final Log log = LogFactory.getLog(DropboxBlobProvider.class); 070 071 private static final String APPLICATION_NAME = "Nuxeo/0"; 072 073 private static final String FILE_CACHE_NAME = "dropbox"; 074 075 private static final String DROPBOX_DOCUMENT_TO_BE_UPDATED_PP = "dropbox_document_to_be_updated"; 076 077 /** 078 * {@link DbxEntry.File} resource cache 079 */ 080 private Cache fileCache; 081 082 @Override 083 public void close() { 084 } 085 086 @Override 087 public Blob readBlob(BlobInfo blobInfo) throws IOException { 088 return new SimpleManagedBlob(blobInfo); 089 } 090 091 @Override 092 public boolean supportsUserUpdate() { 093 return supportsUserUpdateDefaultTrue(); 094 } 095 096 @Override 097 public String writeBlob(Blob blob, Document doc) { 098 throw new UnsupportedOperationException("Writing a blob to Dropbox is not supported"); 099 } 100 101 @Override 102 public URI getURI(ManagedBlob blob, UsageHint usage, HttpServletRequest servletRequest) throws IOException { 103 String url = null; 104 String fileInfo = getFileInfo(blob.getKey()); 105 String user = getUser(fileInfo); 106 String filePath = getFilePath(fileInfo); 107 DbxClient client = getDropboxClient(getCredential(user)); 108 try { 109 switch (usage) { 110 case STREAM: 111 url = client.createTemporaryDirectUrl(filePath).url; 112 break; 113 case DOWNLOAD: 114 url = client.createShareableUrl(filePath); 115 url = url.replace("dl=0", "dl=1"); // enable download flag in url 116 break; 117 case VIEW: 118 url = client.createShareableUrl(filePath); 119 break; 120 } 121 } catch (DbxException e) { 122 throw new IOException("Failed to get Dropbox file URI " + e); 123 } 124 return url != null ? asURI(url) : null; 125 } 126 127 protected InputStream getStream(URI uri) throws IOException { 128 return doGet(uri); 129 } 130 131 @Override 132 public Map<String, URI> getAvailableConversions(ManagedBlob blob, UsageHint hint) throws IOException { 133 return Collections.emptyMap(); 134 } 135 136 @Override 137 public InputStream getThumbnail(ManagedBlob blob) throws IOException { 138 String fileInfo = getFileInfo(blob.getKey()); 139 String user = getUser(fileInfo); 140 String filePath = getFilePath(fileInfo); 141 try { 142 DbxClient.Downloader downloader = getDropboxClient(getCredential(user)).startGetThumbnail(DbxThumbnailSize.w64h64, 143 DbxThumbnailFormat.bestForFileName(filePath, DbxThumbnailFormat.JPEG), filePath, null); 144 145 if (downloader == null) { 146 return null; 147 } 148 return downloader.body; 149 } catch (DbxException e) { 150 throw new IOException("Failed to get Dropbox file thumbnail " + e); 151 } 152 } 153 154 @Override 155 public InputStream getStream(ManagedBlob blob) throws IOException { 156 URI uri = getURI(blob, UsageHint.STREAM, null); 157 return uri == null ? null : getStream(uri); 158 } 159 160 @Override 161 public InputStream getConvertedStream(ManagedBlob blob, String mimeType, DocumentModel doc) throws IOException { 162 Map<String, URI> conversions = getAvailableConversions(blob, UsageHint.STREAM); 163 URI uri = conversions.get(mimeType); 164 if (uri == null) { 165 return null; 166 } 167 return getStream(uri); 168 } 169 170 @Override 171 public ManagedBlob freezeVersion(ManagedBlob blob, Document doc) throws IOException { 172 return null; 173 } 174 175 /** 176 * Gets the blob for a Dropbox file. 177 * 178 * @param fileInfo the file info ({email}:{filePath}) 179 * @return the blob 180 */ 181 protected Blob getBlob(String fileInfo) throws IOException { 182 String user = getUser(fileInfo); 183 String filePath = getFilePath(fileInfo); 184 DbxEntry.File file = getFile(user, filePath); 185 String key = String.format("%s:%s:%s", blobProviderId, user, filePath); 186 BlobInfo blobInfo = new BlobInfo(); 187 blobInfo.key = key; 188 blobInfo.filename = file.name; 189 blobInfo.length = file.numBytes; 190 blobInfo.mimeType = getMimetypeFromFilename(file.name); 191 blobInfo.encoding = null; 192 blobInfo.digest = file.rev; 193 return new SimpleManagedBlob(blobInfo); 194 } 195 196 /** 197 * Removes the prefix from the key. 198 */ 199 protected String getFileInfo(String key) { 200 int colon = key.indexOf(':'); 201 if (colon < 0) { 202 throw new IllegalArgumentException(key); 203 } 204 String fileInfo = key.substring(colon + 1); 205 return fileInfo; 206 } 207 208 protected String getUser(String fileInfo) { 209 return getFileInfoParts(fileInfo)[0]; 210 } 211 212 protected String getFilePath(String fileInfo) { 213 return getFileInfoParts(fileInfo)[1]; 214 } 215 216 protected String[] getFileInfoParts(String fileInfo) { 217 String[] parts = fileInfo.split(":"); 218 if (parts.length != 2) { 219 throw new IllegalArgumentException(fileInfo); 220 } 221 return parts; 222 } 223 224 protected Credential getCredential(String user) throws IOException { 225 return getCredentialFactory().build(user); 226 } 227 228 protected OAuthCredentialFactory getCredentialFactory() { 229 return new OAuthCredentialFactory(getOAuth2Provider()); 230 } 231 232 protected DbxClient getDropboxClient(Credential credential) throws IOException { 233 return getDropboxClient(credential.getAccessToken()); 234 } 235 236 protected DbxClient getDropboxClient(String accessToken) throws IOException { 237 DbxRequestConfig config = new DbxRequestConfig(APPLICATION_NAME, Locale.getDefault().toString()); 238 return new DbxClient(config, accessToken); 239 } 240 241 /** 242 * Retrieves and caches a {@link DbxEntry.File} resource. 243 * 244 * @return a {@link DbxEntry.File} resource. 245 */ 246 protected DbxEntry.File getFile(String user, String filePath) throws IOException { 247 DbxEntry.File fileResource = (DbxEntry.File) getFileCache().get(filePath); 248 if (fileResource == null) { 249 try { 250 DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath); 251 if (fileMetadata == null) { 252 return null; 253 } 254 fileResource = fileMetadata.asFile(); 255 getFileCache().put(filePath, fileResource); 256 } catch (DbxException e) { 257 throw new IOException("Failed to get Dropbox file metadata " + e); 258 } 259 } 260 return fileResource; 261 } 262 263 /** 264 * Executes a GET request 265 */ 266 protected InputStream doGet(URI url) throws IOException { 267 HttpRequestFactory requestFactory = getOAuth2Provider().getRequestFactory(); 268 HttpResponse response = requestFactory.buildGetRequest(new GenericUrl(url)).execute(); 269 return response.getContent(); 270 } 271 272 /** 273 * Parse a {@link URI}. 274 * 275 * @return the {@link URI} or null if it fails 276 */ 277 protected static URI asURI(String link) { 278 try { 279 return new URI(link); 280 } catch (URISyntaxException e) { 281 log.error("Invalid URI: " + link, e); 282 return null; 283 } 284 } 285 286 protected String getClientId() { 287 OAuth2ServiceProvider provider = getOAuth2Provider(); 288 return provider != null ? provider.getClientId() : null; 289 } 290 291 private Cache getFileCache() { 292 if (fileCache == null) { 293 fileCache = Framework.getService(CacheService.class).getCache(FILE_CACHE_NAME); 294 } 295 return fileCache; 296 } 297 298 protected DropboxOAuth2ServiceProvider getOAuth2Provider() { 299 return (DropboxOAuth2ServiceProvider) Framework.getLocalService( 300 OAuth2ServiceProviderRegistry.class).getProvider(blobProviderId); 301 } 302 303 private String getMimetypeFromFilename(String filename) { 304 MimetypeRegistryService mimetypeRegistryService = (MimetypeRegistryService) Framework.getLocalService( 305 MimetypeRegistry.class); 306 return mimetypeRegistryService.getMimetypeFromFilename(filename); 307 } 308 309 @Override 310 public List<DocumentModel> checkChangesAndUpdateBlob(List<DocumentModel> docs) { 311 List<DocumentModel> changedDocuments = new ArrayList<>(); 312 for (DocumentModel doc : docs) { 313 final SimpleManagedBlob blob = (SimpleManagedBlob) doc.getProperty("content").getValue(); 314 if (blob == null) { 315 continue; 316 } 317 if (isVersion(blob)) { 318 continue; 319 } 320 String fileInfo = getFileInfo(blob.key); 321 String user = getUser(fileInfo); 322 String filePath = getFilePath(fileInfo); 323 try { 324 DbxEntry.File file = getFileNoCache(user, filePath); 325 if (StringUtils.isBlank(blob.getDigest()) || !blob.getDigest().equals(file.rev)) { 326 log.trace("Updating " + blob.key); 327 getFileCache().invalidate(filePath); 328 doc.setPropertyValue("content", (SimpleManagedBlob) getBlob(fileInfo)); 329 changedDocuments.add(doc); 330 } 331 332 } catch (DbxException | IOException e) { 333 log.error("Could not update dropbox document " + filePath, e); 334 } 335 } 336 return changedDocuments; 337 } 338 339 protected DbxEntry.File getFileNoCache(String user, String filePath) throws DbxException, IOException { 340 DbxEntry fileMetadata = getDropboxClient(getCredential(user)).getMetadata(filePath); 341 if (fileMetadata == null) { 342 return null; 343 } 344 DbxEntry.File file = fileMetadata.asFile(); 345 return file; 346 } 347 348 @Override 349 public String getPageProviderNameForUpdate() { 350 return DROPBOX_DOCUMENT_TO_BE_UPDATED_PP; 351 } 352 353 @Override 354 public String getBlobProviderId() { 355 return blobProviderId; 356 } 357 358}