From 7f5f67b6fd5036ea6103ff9f77629b584d4832f9 Mon Sep 17 00:00:00 2001
From: tishin-endou
Date: Thu, 28 May 2026 08:57:42 +0800
Subject: [PATCH] feat(s3): support SigV4 in addon
Co-Authored-By: An Qiuyu
---
Dockerfile | 1 -
addons/s3/README.md | 20 ++
addons/s3/apps.py | 1 +
addons/s3/models.py | 85 ++++---
addons/s3/provider.py | 4 +-
addons/s3/requirements.txt | 1 -
addons/s3/settings/__init__.py | 2 +-
addons/s3/settings/defaults.py | 2 +-
addons/s3/static/node-cfg.js | 2 +-
addons/s3/static/s3-rubeus-cfg.js | 83 ++++---
.../s3/static/s3AnonymousLogActionList.json | 20 +-
addons/s3/static/s3LogActionList.json | 20 +-
addons/s3/static/s3NodeConfig.js | 112 +++++-----
addons/s3/static/s3UserConfig.js | 46 ++--
addons/s3/static/settings.json | 2 +-
addons/s3/static/user-cfg.js | 2 +-
addons/s3/templates/s3_credentials_modal.mako | 12 +-
addons/s3/templates/s3_node_settings.mako | 26 +--
addons/s3/templates/s3_user_settings.mako | 14 +-
addons/s3/tests/factories.py | 7 +-
addons/s3/tests/test_model.py | 37 +--
addons/s3/tests/test_serializer.py | 7 +-
addons/s3/tests/test_view.py | 211 +++++++++---------
addons/s3/tests/utils.py | 1 -
addons/s3/utils.py | 130 +++++++----
addons/s3/views.py | 21 +-
requirements.txt | 2 +
27 files changed, 478 insertions(+), 393 deletions(-)
delete mode 100644 addons/s3/requirements.txt
diff --git a/Dockerfile b/Dockerfile
index f23a48e31cb..08e0fc07d22 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -53,7 +53,6 @@ COPY ./addons/onedrive/requirements.txt /code/addons/onedrive/
COPY ./addons/onedrivebusiness/requirements.txt /code/addons/onedrivebusiness/
#COPY ./addons/osfstorage/requirements.txt ./addons/osfstorage/
COPY ./addons/owncloud/requirements.txt ./addons/owncloud/
-COPY ./addons/s3/requirements.txt ./addons/s3/
COPY ./addons/twofactor/requirements.txt ./addons/twofactor/
#COPY ./addons/wiki/requirements.txt ./addons/wiki/
COPY ./addons/zotero/requirements.txt ./addons/zotero/
diff --git a/addons/s3/README.md b/addons/s3/README.md
index 6b4261bd77a..d3347506daa 100644
--- a/addons/s3/README.md
+++ b/addons/s3/README.md
@@ -16,3 +16,23 @@ If you already have an access key and ID, skip this step
2. Scroll down to Configure Add-ons
3. Connect your account and enter your ID and secret
4. Select a bucket to work from, or create a new one.
+
+
+## Creating a restricted-access AWS user for S3 connection
+
+1. Login to AWS
+2. Open Identity and Access Management
+3. Create a user, assign name, set "Access Type" to "Programmatic Access"
+4. Permissions (simple):
+ 1. Add "AmazonS3FullAccess" policy
+5. Permissions (minimal):
+ 1. Click "Create Policy" => opens a new window
+ 2. Make a new policy with the following Actions enabled: `["s3:DeleteObject", "s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:ReplicateObject", "s3:RestoreObject", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:CreateBucket"]`
+ 3. Resources: *I just do "All Resources", but an AWS-educated person would know better about how to narrow it down.
+ 4. Click "Create"
+ 5. Return to "Create User" window
+ 6. Reload policy list
+ 7. Search for your new policy and select.
+6. Tags: *eh*
+7. Click "Review", review, then click "Create user"
+8. Note "Access key ID" and "Secret access key". These are the inputs to the OSF S3 addon.
\ No newline at end of file
diff --git a/addons/s3/apps.py b/addons/s3/apps.py
index 48943b4d87d..ef520daeed1 100644
--- a/addons/s3/apps.py
+++ b/addons/s3/apps.py
@@ -12,6 +12,7 @@
class S3AddonAppConfig(BaseAddonAppConfig):
+ default = True
name = 'addons.s3'
label = 'addons_s3'
full_name = 'Amazon S3'
diff --git a/addons/s3/models.py b/addons/s3/models.py
index b42a1552696..226467776bb 100644
--- a/addons/s3/models.py
+++ b/addons/s3/models.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
from addons.base.models import (BaseOAuthNodeSettings, BaseOAuthUserSettings,
BaseStorageAddon)
from django.db import models
@@ -8,11 +6,18 @@
from addons.base import exceptions
from addons.s3.provider import S3Provider
from addons.s3.serializer import S3Serializer
-from addons.s3.settings import (BUCKET_LOCATIONS,
- ENCRYPT_UPLOADS_DEFAULT)
-from addons.s3.utils import (bucket_exists,
- get_bucket_location_or_error,
- get_bucket_names)
+from addons.s3.settings import (
+ BUCKET_LOCATIONS,
+ ENCRYPT_UPLOADS_DEFAULT
+)
+from addons.s3.utils import (
+ bucket_exists,
+ get_bucket_location_or_error,
+ get_bucket_names,
+ get_bucket_prefixes
+)
+from website.util import api_v2_url
+
class S3FileNode(BaseFileNode):
_provider = 's3'
@@ -53,10 +58,12 @@ def folder_path(self):
@property
def display_name(self):
- return u'{0}: {1}'.format(self.config.full_name, self.folder_id)
+ return f'{self.config.full_name}: {self.folder_id}'
def set_folder(self, folder_id, auth):
- if not bucket_exists(self.external_account.oauth_key, self.external_account.oauth_secret, folder_id):
+ bucket_name = folder_id.split(':')[0]
+
+ if not bucket_exists(self.external_account.oauth_key, self.external_account.oauth_secret, bucket_name):
error_message = ('We are having trouble connecting to that bucket. '
'Try a different one.')
raise exceptions.InvalidFolderError(error_message)
@@ -66,7 +73,7 @@ def set_folder(self, folder_id, auth):
bucket_location = get_bucket_location_or_error(
self.external_account.oauth_key,
self.external_account.oauth_secret,
- folder_id
+ bucket_name
)
try:
bucket_location = BUCKET_LOCATIONS[bucket_location]
@@ -75,32 +82,46 @@ def set_folder(self, folder_id, auth):
# Default to the key. When hit, add mapping to settings
pass
- self.folder_name = '{} ({})'.format(folder_id, bucket_location)
+ self.folder_name = f'{folder_id} ({bucket_location})'
self.save()
- self.nodelogger.log(action='bucket_linked', extra={'bucket': str(folder_id)}, save=True)
+ self.nodelogger.log(action='bucket_linked', extra={'bucket': bucket_name, 'path': self.folder_id}, save=True)
- def get_folders(self, **kwargs):
- # This really gets only buckets, not subfolders,
- # as that's all we want to be linkable on a node.
- try:
+ def get_folders(self, path, folder_id):
+ """
+ Our S3 implementation allows for folder_id to be a bucket's root, or a subfolder in that bucket.
+ """
+ # This is the root, so list all buckets.
+ if not folder_id:
buckets = get_bucket_names(self)
- except Exception:
- raise exceptions.InvalidAuthError()
- return [
- {
+ return [{
'addon': 's3',
'kind': 'folder',
- 'id': bucket,
+ 'id': f'{bucket}:/',
'name': bucket,
- 'path': bucket,
+ 'bucket_name': bucket,
+ 'path': '/',
'urls': {
- 'folders': ''
+ 'folders': api_v2_url(
+ f'nodes/{self.owner._id}/addons/s3/folders/',
+ params={
+ 'id': bucket,
+ 'bucket_name': bucket,
+ }
+ ),
}
- }
- for bucket in buckets
- ]
+ } for bucket in buckets]
+ # This is for a directory for a specific bucket, folders (Prefixes), but not files (Keys) returned, because
+ # these we can only set folders as our base folder_id
+ else:
+ bucket_name, _, path = folder_id.partition(':/')
+ return get_bucket_prefixes(
+ self.external_account.oauth_key,
+ self.external_account.oauth_secret,
+ prefix=path,
+ bucket_name=bucket_name
+ )
@property
def complete(self):
@@ -124,7 +145,7 @@ def deauthorize(self, auth=None, log=True):
def delete(self, save=True):
self.deauthorize(log=False)
- super(NodeSettings, self).delete(save=save)
+ super().delete(save=save)
def serialize_waterbutler_credentials(self):
if not self.has_auth:
@@ -135,10 +156,16 @@ def serialize_waterbutler_credentials(self):
}
def serialize_waterbutler_settings(self):
+ """
+ We use the folder id to hold the bucket location
+ """
if not self.folder_id:
raise exceptions.AddonError('Cannot serialize settings for S3 addon')
+
+ bucket_name = self.folder_id.split(':')[0]
return {
- 'bucket': self.folder_id,
+ 'bucket': bucket_name,
+ 'id': self.folder_id,
'encrypt_uploads': self.encrypt_uploads
}
@@ -146,7 +173,7 @@ def create_waterbutler_log(self, auth, action, metadata):
url = self.owner.web_url_for('addon_view_or_download_file', path=metadata['path'], provider='s3')
self.owner.add_log(
- 's3_{0}'.format(action),
+ f's3_{action}',
auth=auth,
params={
'project': self.owner.parent_id,
diff --git a/addons/s3/provider.py b/addons/s3/provider.py
index ba28d065d54..0321a4a7d4e 100644
--- a/addons/s3/provider.py
+++ b/addons/s3/provider.py
@@ -1,6 +1,6 @@
from addons.s3.serializer import S3Serializer
-class S3Provider(object):
+class S3Provider:
"""An alternative to `ExternalProvider` not tied to OAuth"""
name = 'Amazon S3'
@@ -8,7 +8,7 @@ class S3Provider(object):
serializer = S3Serializer
def __init__(self, account=None):
- super(S3Provider, self).__init__()
+ super().__init__()
# provide an unauthenticated session by default
self.account = account
diff --git a/addons/s3/requirements.txt b/addons/s3/requirements.txt
deleted file mode 100644
index 8e6ed87adf9..00000000000
--- a/addons/s3/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-boto==2.38.0
diff --git a/addons/s3/settings/__init__.py b/addons/s3/settings/__init__.py
index 2b2f98881f6..40f955c5a78 100644
--- a/addons/s3/settings/__init__.py
+++ b/addons/s3/settings/__init__.py
@@ -7,4 +7,4 @@
try:
from .local import * # noqa
except ImportError:
- logger.warn('No local.py settings file found')
+ logger.warning('No local.py settings file found')
diff --git a/addons/s3/settings/defaults.py b/addons/s3/settings/defaults.py
index 0b5cf009214..fbad8ff00a9 100644
--- a/addons/s3/settings/defaults.py
+++ b/addons/s3/settings/defaults.py
@@ -10,7 +10,7 @@
MAX_RENDER_SIZE = (1024 ** 2) * 3
# Max file size permitted by frontend in megabytes
-MAX_UPLOAD_SIZE = 50 * 1024 # 50 GB
+MAX_UPLOAD_SIZE = 5 * 1024 # 5 GB
ALLOWED_ORIGIN = '*'
diff --git a/addons/s3/static/node-cfg.js b/addons/s3/static/node-cfg.js
index c7e33ca410a..ccef5f3e251 100644
--- a/addons/s3/static/node-cfg.js
+++ b/addons/s3/static/node-cfg.js
@@ -4,4 +4,4 @@ var s3NodeConfig = require('./s3NodeConfig.js').s3NodeConfig;
var url = window.contextVars.node.urls.api + 's3/settings/';
-new s3NodeConfig('Amazon S3', '#s3Scope', url, '#s3Grid');
+new s3NodeConfig('Amazon S3', '#s3Scope', url, '#s3Grid');
\ No newline at end of file
diff --git a/addons/s3/static/s3-rubeus-cfg.js b/addons/s3/static/s3-rubeus-cfg.js
index 8493351f9ee..d76dcc8f744 100644
--- a/addons/s3/static/s3-rubeus-cfg.js
+++ b/addons/s3/static/s3-rubeus-cfg.js
@@ -1,47 +1,46 @@
- var Rubeus = require('rubeus');
+var Rubeus = require('rubeus');
- Rubeus.cfg.s3 = {
+Rubeus.cfg.s3 = {
- uploadMethod: 'PUT',
- uploadUrl: null,
- uploadAdded: function(file, item) {
- var self = this;
- var parent = self.getByID(item.parentID);
- var name = file.name;
- // Make it possible to upload into subfolders
- while (parent.depth > 1 && !parent.isAddonRoot) {
- name = parent.name + '/' + name;
- parent = self.getByID(parent.parentID);
- }
- file.destination = name;
- file.signedUrlFrom = parent.urls.upload;
- },
+ uploadMethod: 'PUT',
+ uploadUrl: null,
+ uploadAdded: function (file, item) {
+ var self = this;
+ var parent = self.getByID(item.parentID);
+ var name = file.name;
+ // Make it possible to upload into subfolders
+ while (parent.depth > 1 && !parent.isAddonRoot) {
+ name = parent.name + '/' + name;
+ parent = self.getByID(parent.parentID);
+ }
+ file.destination = name;
+ file.signedUrlFrom = parent.urls.upload;
+ },
- uploadSending: function(file, formData, xhr) {
- xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
- xhr.setRequestHeader('x-amz-acl', 'private');
- },
+ uploadSending: function (file, formData, xhr) {
+ xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
+ xhr.setRequestHeader('x-amz-acl', 'private');
+ },
- uploadSuccess: function(file, row) {
- var self = this;
- var parent = this.getByID(row.parentID);
- row.urls = {
- 'delete': parent.nodeApiUrl + 's3/' + file.destination + '/',
- 'download': parent.nodeUrl + 's3/' + file.destination + '/download/',
- 'view': parent.nodeUrl + 's3/' + file.destination + '/'
- };
- row.permissions = parent.permissions;
- this.updateItem(row);
- var updated = Rubeus.Utils.itemUpdated(row, parent);
- if (updated) {
- self.changeStatus(row, Rubeus.Status.UPDATED);
- self.delayRemoveRow(row);
- } else {
- self.changeStatus(row, Rubeus.Status.UPLOAD_SUCCESS, null, 2000,
- function(row) {
- self.showButtons(row);
- });
- }
+ uploadSuccess: function (file, row) {
+ var self = this;
+ var parent = this.getByID(row.parentID);
+ row.urls = {
+ 'delete': parent.nodeApiUrl + 's3/' + file.destination + '/',
+ 'download': parent.nodeUrl + 's3/' + file.destination + '/download/',
+ 'view': parent.nodeUrl + 's3/' + file.destination + '/'
+ };
+ row.permissions = parent.permissions;
+ this.updateItem(row);
+ var updated = Rubeus.Utils.itemUpdated(row, parent);
+ if (updated) {
+ self.changeStatus(row, Rubeus.Status.UPDATED);
+ self.delayRemoveRow(row);
+ } else {
+ self.changeStatus(row, Rubeus.Status.UPLOAD_SUCCESS, null, 2000,
+ function (row) {
+ self.showButtons(row);
+ });
}
- };
-
+ }
+};
diff --git a/addons/s3/static/s3AnonymousLogActionList.json b/addons/s3/static/s3AnonymousLogActionList.json
index 788ddcfa5fd..9f470e89f03 100644
--- a/addons/s3/static/s3AnonymousLogActionList.json
+++ b/addons/s3/static/s3AnonymousLogActionList.json
@@ -1,11 +1,11 @@
{
- "s3_bucket_linked" : "A user linked an Amazon S3 bucket to a project",
- "s3_bucket_unlinked" : "A user unselected an Amazon S3 bucket in a project",
- "s3_file_added" : "A user added a file to an Amazon S3 bucket in a project",
- "s3_file_removed" : "A user removed a file in an Amazon S3 bucket in a project",
- "s3_file_updated" : "A user updated a file in an Amazon S3 bucket in a project",
- "s3_folder_created" : "A user created a folder in an Amazon S3 in a project",
- "s3_node_authorized" : "A user authorized the Amazon S3 addon for a project",
- "s3_node_deauthorized" : "A user deauthorized the Amazon S3 addon for a project",
- "s3_node_deauthorized_no_user" : "Amazon S3 addon for a project deauthorized"
-}
+ "s3_bucket_linked": "A user linked an Amazon S3 bucket to a project",
+ "s3_bucket_unlinked": "A user unselected an Amazon S3 bucket in a project",
+ "s3_file_added": "A user added a file to an Amazon S3 bucket in a project",
+ "s3_file_removed": "A user removed a file in an Amazon S3 bucket in a project",
+ "s3_file_updated": "A user updated a file in an Amazon S3 bucket in a project",
+ "s3_folder_created": "A user created a folder in an Amazon S3 in a project",
+ "s3_node_authorized": "A user authorized the Amazon S3 addon for a project",
+ "s3_node_deauthorized": "A user deauthorized the Amazon S3 addon for a project",
+ "s3_node_deauthorized_no_user": "Amazon S3 addon for a project deauthorized"
+}
\ No newline at end of file
diff --git a/addons/s3/static/s3LogActionList.json b/addons/s3/static/s3LogActionList.json
index f2dc7ceef05..63d88ca3ba7 100644
--- a/addons/s3/static/s3LogActionList.json
+++ b/addons/s3/static/s3LogActionList.json
@@ -1,11 +1,11 @@
{
- "s3_bucket_linked" : "${user} linked the Amazon S3 bucket ${bucket} to ${node}",
- "s3_bucket_unlinked" : "${user} unselected the Amazon S3 bucket ${bucket} in ${node}",
- "s3_file_added" : "${user} added file ${path} to Amazon S3 bucket ${bucket} in ${node}",
- "s3_file_removed" : "${user} removed ${path} in Amazon S3 bucket ${bucket} in ${node}",
- "s3_file_updated" : "${user} updated file ${path} in Amazon S3 bucket ${bucket} in ${node}",
- "s3_folder_created" : "${user} created folder ${path} in Amazon S3 bucket ${bucket} in ${node}",
- "s3_node_authorized" : "${user} authorized the Amazon S3 addon for ${node}",
- "s3_node_deauthorized" : "${user} deauthorized the Amazon S3 addon for ${node}",
- "s3_node_deauthorized_no_user" : "Amazon S3 addon for ${node} deauthorized"
-}
+ "s3_bucket_linked": "${user} linked the Amazon S3 bucket ${bucket} to ${node}",
+ "s3_bucket_unlinked": "${user} unselected the Amazon S3 bucket ${bucket} in ${node}",
+ "s3_file_added": "${user} added file ${path} to Amazon S3 bucket ${bucket} in ${node}",
+ "s3_file_removed": "${user} removed ${path} in Amazon S3 bucket ${bucket} in ${node}",
+ "s3_file_updated": "${user} updated file ${path} in Amazon S3 bucket ${bucket} in ${node}",
+ "s3_folder_created": "${user} created folder ${path} in Amazon S3 bucket ${bucket} in ${node}",
+ "s3_node_authorized": "${user} authorized the Amazon S3 addon for ${node}",
+ "s3_node_deauthorized": "${user} deauthorized the Amazon S3 addon for ${node}",
+ "s3_node_deauthorized_no_user": "Amazon S3 addon for ${node} deauthorized"
+}
\ No newline at end of file
diff --git a/addons/s3/static/s3NodeConfig.js b/addons/s3/static/s3NodeConfig.js
index f5369691dfb..071deaed584 100644
--- a/addons/s3/static/s3NodeConfig.js
+++ b/addons/s3/static/s3NodeConfig.js
@@ -15,7 +15,7 @@ var OauthAddonFolderPicker = require('js/oauthAddonNodeConfig')._OauthAddonNodeC
var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
bucketLocations: s3Settings.bucketLocations,
- constructor: function(addonName, url, selector, folderPicker, opts, tbOpts) {
+ constructor: function (addonName, url, selector, folderPicker, opts, tbOpts) {
var self = this;
self.super.constructor(addonName, url, selector, folderPicker, tbOpts);
// Non-OAuth fields
@@ -26,9 +26,9 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
{},
OauthAddonFolderPicker.prototype.treebeardOptions,
{ // TreeBeard Options
- columnTitles: function() {
+ columnTitles: function () {
return [{
- title: 'Buckets',
+ title: 'Buckets/Folders',
width: '75%',
sort: false
}, {
@@ -37,30 +37,24 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
sort: false
}];
},
- resolveToggle: function(item) {
- return '';
- },
- resolveIcon: function(item) {
- return m('i.fa.fa-folder-o', ' ');
- },
},
tbOpts
);
},
- connectAccount: function() {
+ connectAccount: function () {
var self = this;
- if( !self.accessKey() && !self.secretKey() ){
+ if (!self.accessKey() && !self.secretKey()) {
self.changeMessage('Please enter both an API access key and secret key.', 'text-danger');
return;
}
- if (!self.accessKey() ){
+ if (!self.accessKey()) {
self.changeMessage('Please enter an API access key.', 'text-danger');
return;
}
- if (!self.secretKey() ){
+ if (!self.secretKey()) {
self.changeMessage('Please enter an API secret key.', 'text-danger');
return;
}
@@ -68,17 +62,17 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
return $osf.postJSON(
self.urls().create, {
- secret_key: self.secretKey(),
- access_key: self.accessKey()
- }
- ).done(function(response) {
+ secret_key: self.secretKey(),
+ access_key: self.accessKey()
+ }
+ ).done(function (response) {
$osf.unblock();
self.clearModal();
$('#s3InputCredentials').modal('hide');
self.changeMessage('Successfully added S3 credentials.', 'text-success', null, true);
self.updateFromData(response);
self.importAuth();
- }).fail(function(xhr, status, error) {
+ }).fail(function (xhr, status, error) {
$osf.unblock();
var message = '';
var response = JSON.parse(xhr.responseText);
@@ -110,7 +104,7 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
* @param {String} bucketName user-provided name of bucket to validate
* @param {Boolean} laxChecking whether to use the more permissive validation
*/
- isValidBucketName: function(bucketName, laxChecking) {
+ isValidBucketName: function (bucketName, laxChecking) {
if (laxChecking === true) {
return /^[a-zA-Z0-9.\-_]{1,255}$/.test(bucketName);
}
@@ -122,7 +116,7 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
},
/** Reset all fields from S3 credentials input modal */
- clearModal: function() {
+ clearModal: function () {
var self = this;
self.message('');
self.messageClass('text-info');
@@ -130,22 +124,22 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
self.accessKey(null);
},
- createBucket: function(self, bucketName, bucketLocation) {
+ createBucket: function (self, bucketName, bucketLocation) {
$osf.block();
bucketName = bucketName.toLowerCase();
return $osf.postJSON(
self.urls().createBucket, {
- bucket_name: bucketName,
- bucket_location: bucketLocation
- }
- ).done(function(response) {
+ bucket_name: bucketName,
+ bucket_location: bucketLocation
+ }
+ ).done(function (response) {
$osf.unblock();
self.loadedFolders(false);
self.activatePicker();
var msg = 'Successfully created bucket "' + $osf.htmlEscape(bucketName) + '". You can now select it from the list.';
var msgType = 'text-success';
self.changeMessage(msg, msgType, null, true);
- }).fail(function(xhr) {
+ }).fail(function (xhr) {
var resp = JSON.parse(xhr.responseText);
var message = resp.message;
var title = resp.title || 'Problem creating bucket';
@@ -156,21 +150,21 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
bootbox.confirm({
title: $osf.htmlEscape(title),
message: $osf.htmlEscape(message),
- callback: function(result) {
+ callback: function (result) {
if (result) {
self.openCreateBucket();
}
},
- buttons:{
- confirm:{
- label:'Try again'
+ buttons: {
+ confirm: {
+ label: 'Try again'
}
}
});
});
},
- openCreateBucket: function() {
+ openCreateBucket: function () {
var self = this;
// Generates html options for key-value pairs in BUCKET_LOCATION_MAP
@@ -187,32 +181,32 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
bootbox.dialog({
title: 'Create a new bucket',
message:
- ' ' +
- '
' +
- '
' +
- '
For more information on locations, click ' +
- 'here' +
- '' +
- '
' +
- '
',
+ '',
buttons: {
cancel: {
label: 'Cancel',
@@ -234,7 +228,7 @@ var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, {
bootbox.confirm({
title: 'Invalid bucket name',
message: 'Amazon S3 buckets can contain lowercase letters, numbers, and hyphens separated by' +
- ' periods. Please try another name.',
+ ' periods. Please try another name.',
callback: function (result) {
if (result) {
self.openCreateBucket();
@@ -271,4 +265,4 @@ function s3NodeConfig(addonName, selector, url, folderPicker, opts, tbOpts) {
module.exports = {
s3NodeConfig: s3NodeConfig,
_s3NodeConfigViewModel: s3FolderPickerViewModel
-};
+};
\ No newline at end of file
diff --git a/addons/s3/static/s3UserConfig.js b/addons/s3/static/s3UserConfig.js
index 5489bc93ca8..ed9c4d433b2 100644
--- a/addons/s3/static/s3UserConfig.js
+++ b/addons/s3/static/s3UserConfig.js
@@ -32,26 +32,26 @@ function ViewModel(url) {
ChangeMessageMixin.call(self);
/** Reset all fields from S3 credentials input modal */
- self.clearModal = function() {
+ self.clearModal = function () {
self.message('');
self.messageClass('text-info');
self.accessKey(null);
self.secretKey(null);
};
/** Send POST request to authorize S3 */
- self.connectAccount = function() {
+ self.connectAccount = function () {
// Selection should not be empty
- if( !self.accessKey() && !self.secretKey() ){
+ if (!self.accessKey() && !self.secretKey()) {
self.changeMessage('Please enter both an API access key and secret key.', 'text-danger');
return;
}
- if (!self.accessKey() ){
+ if (!self.accessKey()) {
self.changeMessage('Please enter an API access key.', 'text-danger');
return;
}
- if (!self.secretKey() ){
+ if (!self.secretKey()) {
self.changeMessage('Please enter an API secret key.', 'text-danger');
return;
}
@@ -61,12 +61,12 @@ function ViewModel(url) {
access_key: self.accessKey,
secret_key: self.secretKey,
})
- ).done(function() {
+ ).done(function () {
self.clearModal();
$modal.modal('hide');
self.updateAccounts();
- }).fail(function(xhr, textStatus, error) {
+ }).fail(function (xhr, textStatus, error) {
var errorMessage = (xhr.status === 400 && xhr.responseJSON.message !== undefined) ? xhr.responseJSON.message : language.authError;
self.changeMessage(errorMessage, 'text-danger');
Raven.captureMessage('Could not authenticate with S3', {
@@ -79,20 +79,20 @@ function ViewModel(url) {
});
};
- self.updateAccounts = function() {
+ self.updateAccounts = function () {
return $.ajax({
url: url,
type: 'GET',
dataType: 'json'
}).done(function (data) {
- self.accounts($.map(data.accounts, function(account) {
- var externalAccount = new ExternalAccount(account);
+ self.accounts($.map(data.accounts, function (account) {
+ var externalAccount = new ExternalAccount(account);
externalAccount.accessKey = account.oauth_key;
externalAccount.secretKey = account.oauth_secret;
return externalAccount;
}));
- $('#s3-header').osfToggleHeight({height: 160});
- }).fail(function(xhr, status, error) {
+ $('#s3-header').osfToggleHeight({ height: 160 });
+ }).fail(function (xhr, status, error) {
self.changeMessage(language.userSettingsError, 'text-danger');
Raven.captureMessage('Error while updating addon account', {
extra: {
@@ -104,7 +104,7 @@ function ViewModel(url) {
});
};
- self.askDisconnect = function(account) {
+ self.askDisconnect = function (account) {
var self = this;
bootbox.confirm({
title: 'Disconnect Amazon S3 Account?',
@@ -117,26 +117,26 @@ function ViewModel(url) {
self.disconnectAccount(account);
}
},
- buttons:{
- confirm:{
- label:'Disconnect',
- className:'btn-danger'
+ buttons: {
+ confirm: {
+ label: 'Disconnect',
+ className: 'btn-danger'
}
}
});
};
- self.disconnectAccount = function(account) {
+ self.disconnectAccount = function (account) {
var self = this;
var url = '/api/v1/oauth/accounts/' + account.id + '/';
var request = $.ajax({
url: url,
type: 'DELETE'
});
- request.done(function(data) {
+ request.done(function (data) {
self.updateAccounts();
});
- request.fail(function(xhr, status, error) {
+ request.fail(function (xhr, status, error) {
Raven.captureMessage('Error while removing addon authorization for ' + account.id, {
extra: {
url: url,
@@ -148,8 +148,8 @@ function ViewModel(url) {
return request;
};
- self.selectionChanged = function() {
- self.changeMessage('','');
+ self.selectionChanged = function () {
+ self.changeMessage('', '');
};
self.updateAccounts();
@@ -170,4 +170,4 @@ function S3UserConfig(selector, url) {
module.exports = {
S3ViewModel: ViewModel,
S3UserConfig: S3UserConfig
-};
+};
\ No newline at end of file
diff --git a/addons/s3/static/settings.json b/addons/s3/static/settings.json
index 05ea9dc29c5..1a5060a1ae4 100644
--- a/addons/s3/static/settings.json
+++ b/addons/s3/static/settings.json
@@ -16,4 +16,4 @@
"sa-east-1": "Sao Paulo"
},
"encryptUploads": true
-}
+}
\ No newline at end of file
diff --git a/addons/s3/static/user-cfg.js b/addons/s3/static/user-cfg.js
index 48e6c1894e5..474aa68ca73 100644
--- a/addons/s3/static/user-cfg.js
+++ b/addons/s3/static/user-cfg.js
@@ -3,4 +3,4 @@ var S3UserConfig = require('./s3UserConfig.js').S3UserConfig;
// Endpoint for S3 user settings
var url = '/api/v1/settings/s3/accounts/';
-var s3UserConfig = new S3UserConfig('#s3AddonScope', url);
+var s3UserConfig = new S3UserConfig('#s3AddonScope', url);
\ No newline at end of file
diff --git a/addons/s3/templates/s3_credentials_modal.mako b/addons/s3/templates/s3_credentials_modal.mako
index de0092a9035..60dc9dc84e7 100644
--- a/addons/s3/templates/s3_credentials_modal.mako
+++ b/addons/s3/templates/s3_credentials_modal.mako
@@ -3,7 +3,7 @@
-
+
\ No newline at end of file
diff --git a/addons/s3/templates/s3_node_settings.mako b/addons/s3/templates/s3_node_settings.mako
index 809d86dbb2e..25a4eb3bf61 100644
--- a/addons/s3/templates/s3_node_settings.mako
+++ b/addons/s3/templates/s3_node_settings.mako
@@ -4,35 +4,35 @@
<%include file="s3_credentials_modal.mako"/>
@@ -55,12 +55,12 @@
-
+
@@ -90,4 +90,4 @@
-
+
\ No newline at end of file
diff --git a/addons/s3/templates/s3_user_settings.mako b/addons/s3/templates/s3_user_settings.mako
index dc4274d9319..5d95c2df2c5 100644
--- a/addons/s3/templates/s3_user_settings.mako
+++ b/addons/s3/templates/s3_user_settings.mako
@@ -6,22 +6,22 @@
<%include file="s3_credentials_modal.mako"/>