GoogleDrive.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import Utils from '../core/Utils'
  2. import Plugin from './Plugin'
  3. import 'whatwg-fetch'
  4. import yo from 'yo-yo'
  5. export default class Google extends Plugin {
  6. constructor (core, opts) {
  7. super(core, opts)
  8. this.type = 'acquirer'
  9. this.id = 'GoogleDrive'
  10. this.titile = 'Google Drive'
  11. this.icon = yo`
  12. <svg class="UppyModalTab-icon" width="28" height="28" viewBox="0 0 16 16">
  13. <path d="M2.955 14.93l2.667-4.62H16l-2.667 4.62H2.955zm2.378-4.62l-2.666 4.62L0 10.31l5.19-8.99 2.666 4.62-2.523 4.37zm10.523-.25h-5.333l-5.19-8.99h5.334l5.19 8.99z"/>
  14. </svg>
  15. `
  16. this.files = []
  17. this.renderBrowserItem = this.renderBrowserItem.bind(this)
  18. this.filterItems = this.filterItems.bind(this)
  19. this.filterQuery = this.filterQuery.bind(this)
  20. this.addFile = this.addFile.bind(this)
  21. this.getFolder = this.getFolder.bind(this)
  22. this.handleClick = this.handleClick.bind(this)
  23. this.logout = this.logout.bind(this)
  24. this.renderBrowser = this.renderBrowser.bind(this)
  25. this.sortByTitle = this.sortByTitle.bind(this)
  26. this.sortByDate = this.sortByDate.bind(this)
  27. this.render = this.render.bind(this)
  28. // set default options
  29. const defaultOptions = {}
  30. // merge default options with the ones set by user
  31. this.opts = Object.assign({}, defaultOptions, opts)
  32. }
  33. install () {
  34. // Set default state for Google Drive
  35. this.core.setState({
  36. googleDrive: {
  37. authenticated: false,
  38. files: [],
  39. folders: [],
  40. directory: [{
  41. title: 'My Drive',
  42. id: 'root'
  43. }],
  44. active: {},
  45. filterInput: ''
  46. }
  47. })
  48. const target = this.opts.target
  49. const plugin = this
  50. this.target = this.mount(target, plugin)
  51. this.checkAuthentication()
  52. .then((authenticated) => {
  53. this.updateState({authenticated})
  54. if (authenticated) {
  55. return this.getFolder(this.core.getState().googleDrive.directory.id)
  56. }
  57. return authenticated
  58. })
  59. .then((newState) => {
  60. this.updateState(newState)
  61. })
  62. return
  63. }
  64. focus () {
  65. const firstInput = document.querySelector(`${this.target} .UppyGoogleDrive-focusInput`)
  66. // only works for the first time if wrapped in setTimeout for some reason
  67. // firstInput.focus()
  68. setTimeout(function () {
  69. firstInput.focus()
  70. }, 10)
  71. }
  72. /**
  73. * Little shorthand to update the state with my new state
  74. */
  75. updateState (newState) {
  76. const {state} = this.core
  77. const googleDrive = Object.assign({}, state.googleDrive, newState)
  78. this.core.setState({googleDrive})
  79. }
  80. /**
  81. * Check to see if the user is authenticated.
  82. * @return {Promise} authentication status
  83. */
  84. checkAuthentication () {
  85. return fetch(`${this.opts.host}/google/authorize`, {
  86. method: 'get',
  87. credentials: 'include',
  88. headers: {
  89. 'Accept': 'application/json',
  90. 'Content-Type': 'application/json'
  91. }
  92. })
  93. .then((res) => {
  94. if (res.status >= 200 && res.status <= 300) {
  95. return res.json()
  96. } else {
  97. let error = new Error(res.statusText)
  98. error.response = res
  99. throw error
  100. }
  101. })
  102. .then((data) => data.isAuthenticated)
  103. .catch((err) => err)
  104. }
  105. /**
  106. * Based on folder ID, fetch a new folder
  107. * @param {String} id Folder id
  108. * @return {Promise} Folders/files in folder
  109. */
  110. getFolder (id = 'root') {
  111. return fetch(`${this.opts.host}/google/list?dir=${id}`, {
  112. method: 'get',
  113. credentials: 'include',
  114. headers: {
  115. 'Accept': 'application/json',
  116. 'Content-Type': 'application/json'
  117. }
  118. })
  119. .then((res) => {
  120. if (res.status >= 200 && res.status <= 300) {
  121. return res.json().then((data) => {
  122. // let result = Utils.groupBy(data.items, (item) => item.mimeType)
  123. let folders = []
  124. let files = []
  125. data.items.forEach((item) => {
  126. if (item.mimeType === 'application/vnd.google-apps.folder') {
  127. folders.push(item)
  128. } else {
  129. files.push(item)
  130. }
  131. })
  132. return {
  133. folders,
  134. files
  135. }
  136. })
  137. } else {
  138. let error = new Error(res.statusText)
  139. error.response = res
  140. throw error
  141. }
  142. })
  143. .catch((err) => {
  144. return err
  145. })
  146. }
  147. /**
  148. * Fetches new folder and adds to breadcrumb nav
  149. * @param {String} id Folder id
  150. * @param {String} title Folder title
  151. */
  152. getSubFolder (id, title) {
  153. this.getFolder(id)
  154. .then((data) => {
  155. const state = this.core.getState().googleDrive
  156. const index = state.directory.findIndex((dir) => id === dir.id)
  157. let directory
  158. if (index !== -1) {
  159. directory = state.directory.slice(0, index + 1)
  160. } else {
  161. directory = state.directory.concat([{
  162. id,
  163. title
  164. }])
  165. }
  166. this.updateState(Utils.extend(data, {directory}))
  167. })
  168. }
  169. /**
  170. * Will soon be replaced by actual Uppy file handling.
  171. * Requests the server download the selected file.
  172. * @param {String} fileId
  173. * @return {Promise} Result
  174. */
  175. addFile (file) {
  176. const tagFile = {
  177. source: this,
  178. data: file,
  179. name: file.title,
  180. type: this.getFileType(file),
  181. isRemote: true,
  182. remote: {
  183. url: `${this.opts.host}/google/get?fileId=${file.id}`,
  184. body: {
  185. fileId: file.id
  186. }
  187. }
  188. }
  189. this.core.emitter.emit('file-add', tagFile)
  190. }
  191. handleUploadError (response) {
  192. this.checkAuthentication()
  193. .then((authenticated) => {
  194. this.updateState({authenticated})
  195. })
  196. }
  197. /**
  198. * Removes session token on client side.
  199. */
  200. logout () {
  201. fetch(`${this.opts.host}/google/logout?redirect=${location.href}`, {
  202. method: 'get',
  203. credentials: 'include',
  204. headers: {
  205. 'Accept': 'application/json',
  206. 'Content-Type': 'application/json'
  207. }
  208. })
  209. .then((res) => res.json())
  210. .then((res) => {
  211. if (res.ok) {
  212. console.log('ok')
  213. const newState = {
  214. authenticated: false,
  215. files: [],
  216. folders: [],
  217. directory: {
  218. title: 'My Drive',
  219. id: 'root'
  220. }
  221. }
  222. this.updateState(newState)
  223. }
  224. })
  225. }
  226. getFileType (file) {
  227. const fileTypes = {
  228. 'application/vnd.google-apps.folder': 'Folder',
  229. 'application/vnd.google-apps.document': 'Google Docs',
  230. 'application/vnd.google-apps.spreadsheet': 'Google Sheets',
  231. 'application/vnd.google-apps.presentation': 'Google Slides',
  232. 'image/jpeg': 'JPEG Image',
  233. 'image/png': 'PNG Image'
  234. }
  235. return fileTypes[file.mimeType] ? fileTypes[file.mimeType] : file.fileExtension.toUpperCase()
  236. }
  237. /**
  238. * Used to set active file/folder.
  239. * @param {Object} file Active file/folder
  240. */
  241. handleClick (file) {
  242. const state = this.core.getState().googleDrive
  243. const newState = Object.assign({}, state, {
  244. active: file
  245. })
  246. this.updateState(newState)
  247. }
  248. filterQuery (e) {
  249. const state = this.core.getState().googleDrive
  250. this.updateState(Object.assign({}, state, {
  251. filterInput: e.target.value
  252. }))
  253. }
  254. filterItems (items) {
  255. const state = this.core.getState().googleDrive
  256. return items.filter((folder) => {
  257. return folder.title.toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1
  258. })
  259. }
  260. sortByTitle () {
  261. const state = this.core.getState().googleDrive
  262. const {files, folders, sorting} = state
  263. let sortedFiles = files.sort((fileA, fileB) => {
  264. if (sorting === 'titleDescending') {
  265. return fileB.title.localeCompare(fileA.title)
  266. }
  267. return fileA.title.localeCompare(fileB.title)
  268. })
  269. let sortedFolders = folders.sort((folderA, folderB) => {
  270. if (sorting === 'titleDescending') {
  271. return folderB.title.localeCompare(folderA.title)
  272. }
  273. return folderA.title.localeCompare(folderB.title)
  274. })
  275. this.updateState(Object.assign({}, state, {
  276. files: sortedFiles,
  277. folders: sortedFolders,
  278. sorting: (sorting === 'titleDescending') ? 'titleAscending' : 'titleDescending'
  279. }))
  280. }
  281. sortByDate () {
  282. const state = this.core.getState().googleDrive
  283. const {files, folders, sorting} = state
  284. let sortedFiles = files.sort((fileA, fileB) => {
  285. let a = new Date(fileA.modifiedByMeDate)
  286. let b = new Date(fileB.modifiedByMeDate)
  287. if (sorting === 'dateDescending') {
  288. return a > b ? -1 : a < b ? 1 : 0
  289. }
  290. return a > b ? 1 : a < b ? -1 : 0
  291. })
  292. let sortedFolders = folders.sort((folderA, folderB) => {
  293. let a = new Date(folderA.modifiedByMeDate)
  294. let b = new Date(folderB.modifiedByMeDate)
  295. if (sorting === 'dateDescending') {
  296. return a > b ? -1 : a < b ? 1 : 0
  297. }
  298. return a > b ? 1 : a < b ? -1 : 0
  299. })
  300. this.updateState(Object.assign({}, state, {
  301. files: sortedFiles,
  302. folders: sortedFolders,
  303. sorting: (sorting === 'dateDescending') ? 'dateAscending' : 'dateDescending'
  304. }))
  305. }
  306. /**
  307. * Render user authentication view
  308. */
  309. renderAuth () {
  310. const state = btoa(JSON.stringify({
  311. redirect: location.href.split('#')[0]
  312. }))
  313. const link = `${this.opts.host}/connect/google?state=${state}`
  314. return yo`
  315. <div class="UppyGoogleDrive-authenticate">
  316. <h1>You need to authenticate with Google before selecting files.</h1>
  317. <a href=${link}>Authenticate</a>
  318. </div>
  319. `
  320. }
  321. /**
  322. * Render file browser
  323. * @param {Object} state Google Drive state
  324. */
  325. renderBrowser (state) {
  326. let folders = state.folders
  327. let files = state.files
  328. let previewElem = ''
  329. const isFileSelected = Object.keys(state.active).length !== 0 && JSON.stringify(state.active) !== JSON.stringify({})
  330. if (state.filterInput !== '') {
  331. folders = this.filterItems(state.folders)
  332. files = this.filterItems(state.files)
  333. }
  334. folders = folders.map((folder) => this.renderBrowserItem(folder))
  335. files = files.map((file) => this.renderBrowserItem(file))
  336. const breadcrumbs = state.directory.map((dir) => yo`<li><button onclick=${this.getSubFolder.bind(this, dir.id, dir.title)}>${dir.title}</button></li> `)
  337. if (isFileSelected) {
  338. previewElem = yo`
  339. <div>
  340. <h1><span class="UppyGoogleDrive-fileIcon"><img src=${state.active.iconLink}/></span>${state.active.title}</h1>
  341. <ul>
  342. <li>Type: ${this.getFileType(state.active)}</li>
  343. <li>Modified By Me: ${state.active.modifiedByMeDate}</li>
  344. </ul>
  345. ${state.active.thumbnailLink ? yo`<img src=${state.active.thumbnailLink} class="UppyGoogleDrive-fileThumbnail" />` : yo``}
  346. </div>
  347. `
  348. }
  349. return yo`
  350. <div>
  351. <div class="UppyGoogleDrive-header">
  352. <ul class="UppyGoogleDrive-breadcrumbs">
  353. ${breadcrumbs}
  354. </ul>
  355. </div>
  356. <div class="container-fluid">
  357. <div class="row">
  358. <div class="hidden-md-down col-lg-3 col-xl-3">
  359. <ul class="UppyGoogleDrive-sidebar">
  360. <li class="UppyGoogleDrive-filter"><input class="UppyGoogleDrive-focusInput" type='text' onkeyup=${this.filterQuery} placeholder="Search.." value=${state.filterInput}/></li>
  361. <li><button onclick=${this.getSubFolder.bind(this, 'root', 'My Drive')}><img src="https://ssl.gstatic.com/docs/doclist/images/icon_11_collection_list_3.png"/> My Drive</button></li>
  362. <li><button><img src="https://ssl.gstatic.com/docs/doclist/images/icon_11_shared_collection_list_1.png"/> Shared with me</button></li>
  363. <li><button onclick=${this.logout}>Logout</button></li>
  364. </ul>
  365. </div>
  366. <div class="col-md-12 col-lg-9 col-xl-6">
  367. <div class="UppyGoogleDrive-browserContainer">
  368. <table class="UppyGoogleDrive-browser">
  369. <thead>
  370. <tr>
  371. <td class="UppyGoogleDrive-sortableHeader" onclick=${this.sortByTitle}>Name</td>
  372. <td>Owner</td>
  373. <td class="UppyGoogleDrive-sortableHeader" onclick=${this.sortByDate}>Last Modified</td>
  374. <td>Filesize</td>
  375. </tr>
  376. </thead>
  377. <tbody>
  378. ${folders}
  379. ${files}
  380. </tbody>
  381. </table>
  382. </div>
  383. </div>
  384. <div class="hidden-lg-down col-xl-2">
  385. <div class="UppyGoogleDrive-fileInfo">
  386. ${previewElem}
  387. </div>
  388. </div>
  389. </div>
  390. </div>
  391. </div>
  392. `
  393. }
  394. renderBrowserItem (item) {
  395. const state = this.core.getState().googleDrive
  396. const isAFileSelected = Object.keys(state.active).length !== 0 && JSON.stringify(state.active) !== JSON.stringify({})
  397. const isFolder = item.mimeType === 'application/vnd.google-apps.folder'
  398. return yo`
  399. <tr class=${(isAFileSelected && state.active.id === item.id) ? 'is-active' : ''}
  400. onclick=${this.handleClick.bind(this, item)}
  401. ondblclick=${isFolder ? this.getSubFolder.bind(this, item.id, item.title) : this.addFile.bind(this, item)}>
  402. <td><span class="UppyGoogleDrive-folderIcon"><img src=${item.iconLink}/></span> ${item.title}</td>
  403. <td>Me</td>
  404. <td>${item.modifiedByMeDate}</td>
  405. <td>-</td>
  406. </tr>
  407. `
  408. }
  409. renderError (err) {
  410. return `Something went wrong. Probably our fault. ${err}`
  411. }
  412. render (state) {
  413. if (state.googleDrive.authenticated) {
  414. return this.renderBrowser(state.googleDrive)
  415. } else {
  416. return this.renderAuth()
  417. }
  418. }
  419. }