Instances that wish to enable Google Drive support should first enable it to only a small subset of users (100 maximum) to allow the OAuth screen to be verified by Google without hitting the user limit. Minimum level in the config can be set to enable beta testing of this feature and then disabled by setting it to None. - Add user level assignment screen to the administration panel - Add privacy policy and links to it in various places - Add switch accounts link near the Google Drive picker
272 lines
7.5 KiB
JavaScript
272 lines
7.5 KiB
JavaScript
class Gpicker{
|
|
constructor(){
|
|
this.apiKey = gameConfig.google_credentials.api_key
|
|
this.oauthClientId = gameConfig.google_credentials.oauth_client_id
|
|
this.projectNumber = gameConfig.google_credentials.project_number
|
|
this.scope = "https://www.googleapis.com/auth/drive.readonly"
|
|
this.folder = "application/vnd.google-apps.folder"
|
|
this.filesUrl = "https://www.googleapis.com/drive/v3/files/"
|
|
this.resolveQueue = []
|
|
this.queueActive = false
|
|
}
|
|
browse(lockedCallback, errorCallback){
|
|
return this.loadApi()
|
|
.then(() => this.getToken(lockedCallback, errorCallback))
|
|
.then(() => new Promise((resolve, reject) => {
|
|
this.displayPicker(data => {
|
|
if(data.action === "picked"){
|
|
var file = data.docs[0]
|
|
var folders = []
|
|
var rateLimit = -1
|
|
var lastBatch = 0
|
|
var walk = (files, output=[]) => {
|
|
for(var i = 0; i < files.length; i++){
|
|
var path = files[i].path ? files[i].path + "/" : ""
|
|
var list = files[i].list
|
|
if(!list){
|
|
continue
|
|
}
|
|
for(var j = 0; j < list.length; j++){
|
|
var file = list[j]
|
|
if(file.mimeType === this.folder){
|
|
folders.push({
|
|
path: path + file.name,
|
|
id: file.id
|
|
})
|
|
}else{
|
|
output.push(new GdriveFile({
|
|
path: path + file.name,
|
|
name: file.name,
|
|
id: file.id
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
var batchList = []
|
|
for(var i = 0; i < folders.length && batchList.length < 100; i++){
|
|
if(!folders[i].listed){
|
|
folders[i].pos = i
|
|
folders[i].listed = true
|
|
batchList.push(folders[i])
|
|
}
|
|
}
|
|
if(batchList.length){
|
|
var batch = gapi.client.newBatch()
|
|
batchList.forEach(folder => {
|
|
var req = {
|
|
q: "'" + folder.id + "' in parents and trashed = false",
|
|
orderBy: "name_natural"
|
|
}
|
|
if(folder.pageToken){
|
|
req.pageToken = folder.pageToken
|
|
}
|
|
batch.add(gapi.client.drive.files.list(req), {id: folder.pos})
|
|
})
|
|
if(lastBatch + batchList.length > 100){
|
|
var waitPromise = this.sleep(1000)
|
|
}else{
|
|
var waitPromise = Promise.resolve()
|
|
}
|
|
return waitPromise.then(() => this.queue()).then(() => batch.then(responses => {
|
|
var files = []
|
|
var rateLimited = false
|
|
for(var i in responses.result){
|
|
var result = responses.result[i].result
|
|
if(result.error){
|
|
if(result.error.errors[0].domain !== "usageLimits"){
|
|
console.warn(result)
|
|
}else if(!rateLimited){
|
|
rateLimited = true
|
|
rateLimit++
|
|
folders.push({
|
|
path: folders[i].path,
|
|
id: folders[i].id,
|
|
pageToken: folders[i].pageToken
|
|
})
|
|
}
|
|
}else{
|
|
if(result.nextPageToken){
|
|
folders.push({
|
|
path: folders[i].path,
|
|
id: folders[i].id,
|
|
pageToken: result.nextPageToken
|
|
})
|
|
}
|
|
files.push({path: folders[i].path, list: result.files})
|
|
}
|
|
}
|
|
if(rateLimited){
|
|
return this.sleep(Math.pow(2, rateLimit) * 1000).then(() => walk(files, output))
|
|
}else{
|
|
return walk(files, output)
|
|
}
|
|
}))
|
|
}else{
|
|
return output
|
|
}
|
|
}
|
|
if(file.mimeType === this.folder){
|
|
return walk([{list: [file]}]).then(resolve, reject)
|
|
}else{
|
|
return reject("cancel")
|
|
}
|
|
}else if(data.action === "cancel"){
|
|
return reject("cancel")
|
|
}
|
|
})
|
|
}))
|
|
}
|
|
loadApi(){
|
|
if(window.gapi && gapi.client && gapi.client.drive){
|
|
return Promise.resolve()
|
|
}
|
|
return loader.loadScript("https://apis.google.com/js/api.js")
|
|
.then(() => new Promise((resolve, reject) =>
|
|
gapi.load("auth2:picker:client", {
|
|
callback: resolve,
|
|
onerror: reject
|
|
})
|
|
))
|
|
.then(() => new Promise((resolve, reject) =>
|
|
gapi.client.load("drive", "v3").then(resolve, reject)
|
|
))
|
|
}
|
|
getAuth(errorCallback=()=>{}){
|
|
if(!this.auth){
|
|
return new Promise((resolve, reject) => {
|
|
gapi.auth2.init({
|
|
clientId: this.oauthClientId,
|
|
fetch_basic_profile: false,
|
|
scope: this.scope
|
|
}).then(() => {
|
|
this.auth = gapi.auth2.getAuthInstance()
|
|
resolve(this.auth)
|
|
}, e => {
|
|
if(e.details){
|
|
var errorStr = strings.gpicker.authError.replace("%s", e.details)
|
|
if(/cookie/i.test(e.details)){
|
|
errorStr += "\n\n" + strings.gpicker.cookieError
|
|
}
|
|
errorCallback(errorStr)
|
|
}
|
|
reject(e)
|
|
})
|
|
})
|
|
}else{
|
|
return Promise.resolve(this.auth)
|
|
}
|
|
}
|
|
getToken(lockedCallback=()=>{}, errorCallback=()=>{}, force){
|
|
if(this.oauthToken && !force){
|
|
return Promise.resolve()
|
|
}
|
|
return this.getAuth(errorCallback).then(auth => {
|
|
var user = force || auth.currentUser.get()
|
|
if(force || !this.checkScope(user)){
|
|
lockedCallback(false)
|
|
return auth.signIn(force ? {
|
|
prompt: "select_account"
|
|
} : undefined).then(user => {
|
|
if(this.checkScope(user)){
|
|
lockedCallback(true)
|
|
}else{
|
|
return Promise.reject("cancel")
|
|
}
|
|
}, () => Promise.reject("cancel"))
|
|
}
|
|
})
|
|
}
|
|
checkScope(user){
|
|
if(user.hasGrantedScopes(this.scope)){
|
|
this.oauthToken = user.getAuthResponse(true).access_token
|
|
return this.oauthToken
|
|
}else{
|
|
return false
|
|
}
|
|
}
|
|
switchAccounts(lockedCallback, errorCallback){
|
|
return this.loadApi().then(() => this.getToken(lockedCallback, errorCallback, true))
|
|
}
|
|
displayPicker(callback){
|
|
var picker = gapi.picker.api
|
|
new picker.PickerBuilder()
|
|
.setDeveloperKey(this.apiKey)
|
|
.setAppId(this.projectNumber)
|
|
.setOAuthToken(this.oauthToken)
|
|
.setLocale(strings.gpicker.locale)
|
|
.hideTitleBar()
|
|
.addView(new picker.DocsView("folders")
|
|
.setLabel(strings.gpicker.myDrive)
|
|
.setParent("root")
|
|
.setSelectFolderEnabled(true)
|
|
.setMode("grid")
|
|
)
|
|
.addView(new picker.DocsView("folders")
|
|
.setLabel(strings.gpicker.starred)
|
|
.setStarred(true)
|
|
.setSelectFolderEnabled(true)
|
|
.setMode("grid")
|
|
)
|
|
.addView(new picker.DocsView("folders")
|
|
.setLabel(strings.gpicker.sharedWithMe)
|
|
.setOwnedByMe(false)
|
|
.setSelectFolderEnabled(true)
|
|
.setMode("list")
|
|
)
|
|
.setCallback(callback)
|
|
.setSize(Infinity, Infinity)
|
|
.build()
|
|
.setVisible(true)
|
|
}
|
|
downloadFile(id, arrayBuffer, retry){
|
|
var url = this.filesUrl + id + "?alt=media"
|
|
return this.queue().then(this.getToken.bind(this)).then(() =>
|
|
loader.ajax(url, request => {
|
|
if(arrayBuffer){
|
|
request.responseType = "arraybuffer"
|
|
}
|
|
request.setRequestHeader("Authorization", "Bearer " + this.oauthToken)
|
|
}, true).then(event => {
|
|
var request = event.target
|
|
var reject = () => Promise.reject(`${url} (${request.status})`)
|
|
if(request.status === 200){
|
|
return request.response
|
|
}else if(request.status === 401 && !retry){
|
|
return new Response(request.response).json().then(response => {
|
|
var e = response.error
|
|
if(e && e.errors[0].reason === "authError"){
|
|
delete this.oauthToken
|
|
return this.downloadFile(id, arrayBuffer, true)
|
|
}else{
|
|
return reject()
|
|
}
|
|
}, reject)
|
|
}
|
|
return reject()
|
|
})
|
|
)
|
|
}
|
|
sleep(time){
|
|
return new Promise(resolve => setTimeout(resolve, time))
|
|
}
|
|
queue(){
|
|
return new Promise(resolve => {
|
|
this.resolveQueue.push(resolve)
|
|
if(!this.queueActive){
|
|
this.queueActive = true
|
|
this.queueTimer = setInterval(this.parseQueue.bind(this), 100)
|
|
this.parseQueue()
|
|
}
|
|
})
|
|
}
|
|
parseQueue(){
|
|
if(this.resolveQueue.length){
|
|
var resolve = this.resolveQueue.shift()
|
|
resolve()
|
|
}else{
|
|
this.queueActive = false
|
|
clearInterval(this.queueTimer)
|
|
}
|
|
}
|
|
}
|