소스 검색

Merge pull request #2145 from transloadit/gsuite-files

companion: add support to download gsuite (google docs, google spreadsheet files
Ifedapo .A. Olarewaju 5 년 전
부모
커밋
a133903402

+ 2 - 4
packages/@uppy/companion/src/server/provider/drive/adapter.js

@@ -29,7 +29,7 @@ exports.getItemIcon = (item) => {
       : `${item.backgroundImageLink}${size}`
   }
 
-  if (item.thumbnailLink) {
+  if (item.thumbnailLink && !item.mimeType.startsWith('application/vnd.google')) {
     const smallerThumbnailLink = item.thumbnailLink.replace('s220', 's40')
     return smallerThumbnailLink
   }
@@ -38,9 +38,7 @@ exports.getItemIcon = (item) => {
 }
 
 exports.getItemSubList = (item) => {
-  return item.files.filter((i) => {
-    return exports.isFolder(i) || !i.mimeType.startsWith('application/vnd.google')
-  })
+  return item.files
 }
 
 exports.getItemName = (item) => {

+ 72 - 10
packages/@uppy/companion/src/server/provider/drive/index.js

@@ -102,19 +102,58 @@ class Drive extends Provider {
       .request(done)
   }
 
-  download ({ id, token }, onData) {
+  _exportGsuiteFile ({ id, token }) {
+    logger.info(`calling google file export for ${id}`, 'provider.drive.export')
     return this.client
       .query()
-      .get(`files/${id}`)
-      .qs({ alt: 'media', supportsAllDrives: true })
+      .get(`files/${id}/export`)
+      .qs({ supportsAllDrives: true, mimeType: 'application/pdf' })
       .auth(token)
       .request()
-      .on('data', (chunk) => onData(null, chunk))
-      .on('end', () => onData(null, null))
-      .on('error', (err) => {
-        logger.error(err, 'provider.drive.download.error')
+  }
+
+  _getGsuiteFileMeta ({ id, token }, onDone) {
+    logger.info(`calling Gsuite file meta for ${id}`, 'provider.drive.export.meta')
+    return this.client
+      .query()
+      .head(`files/${id}/export`)
+      .qs({ supportsAllDrives: true, mimeType: 'application/pdf' })
+      .auth(token)
+      .request(onDone)
+  }
+
+  _isGsuiteFile (mimeType) {
+    return mimeType.startsWith('application/vnd.google')
+  }
+
+  download ({ id, token }, onData) {
+    this.stats({ id, token }, (err, resp, body) => {
+      if (err) {
+        logger.error(err, 'provider.drive.download.stats.error')
         onData(err)
-      })
+        return
+      }
+
+      let requestStream
+      if (this._isGsuiteFile(body.mimeType)) {
+        requestStream = this._exportGsuiteFile({ id, token })
+      } else {
+        requestStream = this.client
+          .query()
+          .get(`files/${id}`)
+          .qs({ alt: 'media', supportsAllDrives: true })
+          .auth(token)
+          .request()
+      }
+
+      requestStream
+        .on('data', (chunk) => onData(null, chunk))
+        .on('end', () => onData(null, null))
+        .on('error', (err) => {
+          logger.error(err, 'provider.drive.download.error')
+          onData(err)
+        })
+    })
   }
 
   thumbnail (_, done) {
@@ -131,7 +170,30 @@ class Drive extends Provider {
         logger.error(err, 'provider.drive.size.error')
         return done(err)
       }
-      done(null, parseInt(body.size))
+
+      if (this._isGsuiteFile(body.mimeType)) {
+        // Google Docs file sizes can be determined
+        // while Google sheets file sizes can't be determined
+        const googleDocMimeType = 'application/vnd.google-apps.document'
+        if (body.mimeType !== googleDocMimeType) {
+          const maxExportFileSize = 10 * 1024 * 1024 // 10 MB
+          done(null, maxExportFileSize)
+          return
+        }
+
+        this._getGsuiteFileMeta({ id, token }, (err, resp) => {
+          if (err || resp.statusCode !== 200) {
+            err = this._error(err, resp)
+            logger.error(err, 'provider.drive.docs.size.error')
+            return done(err)
+          }
+
+          const size = resp.headers['content-length']
+          done(null, size ? parseInt(size) : null)
+        })
+      } else {
+        done(null, parseInt(body.size))
+      }
     })
   }
 
@@ -183,7 +245,7 @@ class Drive extends Provider {
   _error (err, resp) {
     if (resp) {
       const fallbackMessage = `request to ${this.authProvider} returned ${resp.statusCode}`
-      const errMsg = resp.body.error ? resp.body.error.message : fallbackMessage
+      const errMsg = (resp.body && resp.body.error) ? resp.body.error.message : fallbackMessage
       return resp.statusCode === 401 ? new ProviderAuthError() : new ProviderApiError(errMsg, resp.statusCode)
     }
     return err

+ 33 - 12
packages/@uppy/companion/test/__mocks__/purest.js

@@ -2,10 +2,17 @@ const fs = require('fs')
 
 class MockPurest {
   constructor (opts) {
-    const methodsToMock = ['query', 'select', 'where', 'qs', 'auth', 'get', 'put', 'post', 'options', 'json']
+    const methodsToMock = ['query', 'select', 'where', 'qs', 'auth', 'json']
+    const httpMethodsToMock = ['get', 'put', 'post', 'options', 'head']
     methodsToMock.forEach((item) => {
       this[item] = () => this
     })
+    httpMethodsToMock.forEach((item) => {
+      this[item] = (url) => {
+        this._requestUrl = url
+        return this
+      }
+    })
     this.opts = opts
   }
 
@@ -13,24 +20,38 @@ class MockPurest {
     if (typeof done === 'function') {
       const responses = {
         dropbox: {
-          hash: '0a9f95a989dd4b1851f0103c31e304ce',
-          user_email: 'foo@bar.com',
-          email: 'foo@bar.com',
-          entries: [{ rev: 'f24234cd4' }]
+          default: {
+            hash: '0a9f95a989dd4b1851f0103c31e304ce',
+            user_email: 'foo@bar.com',
+            email: 'foo@bar.com',
+            entries: [{ rev: 'f24234cd4' }]
+          }
         },
         drive: {
-          kind: 'drive#fileList',
-          etag: '"bcIyJ9A3gXa8oTYmz6nzAjQd-lY/eQc3WbZHkXpcItNyGKDuKXM_bNY"',
-          files: [{
+          'files/README.md': {
             id: '0B2x-PmqQHSKdT013TE1VVjZ3TWs',
             mimeType: 'image/jpg',
             ownedByMe: true,
-            permissions: [{ role: 'owner', emailAddress: 'ife@bala.com' }]
-          }],
-          size: 300
+            permissions: [{ role: 'owner', emailAddress: 'ife@bala.com' }],
+            size: 300,
+            kind: 'drive#file',
+            etag: '"bcIyJ9A3gXa8oTYmz6nzAjQd-lY/eQc3WbZHkXpcItNyGKDuKXM_bNY"'
+          },
+          default: {
+            kind: 'drive#fileList',
+            etag: '"bcIyJ9A3gXa8oTYmz6nzAjQd-lY/eQc3WbZHkXpcItNyGKDuKXM_bNY"',
+            files: [{
+              id: '0B2x-PmqQHSKdT013TE1VVjZ3TWs',
+              mimeType: 'image/jpg',
+              ownedByMe: true,
+              permissions: [{ role: 'owner', emailAddress: 'ife@bala.com' }]
+            }],
+            size: 300
+          }
         }
       }
-      const body = responses[this.opts.providerName]
+      const providerResponses = responses[this.opts.providerName]
+      const body = providerResponses[this._requestUrl] || providerResponses.default
       done(null, { body, statusCode: 200 }, body)
     }
 

+ 0 - 14
packages/@uppy/companion/test/__tests__/http-agent.js

@@ -91,18 +91,4 @@ describe('test protected request Agent', () => {
       done()
     })
   })
-
-  test('blocks URLs that have DNS pinned to a private IP address', (done) => {
-    const options = {
-      uri: 'http://127.0.0.1.xip.io:8090',
-      method: 'GET',
-      agentClass: getProtectedHttpAgent('http', true)
-    }
-
-    request(options, (err) => {
-      expect(err).toBeTruthy()
-      expect(err.message.startsWith(FORBIDDEN_IP_ADDRESS)).toEqual(true)
-      done()
-    })
-  })
 })