Repository: RafalWilinski/s3-uploader
Branch: master
Commit: a0d81fe669a4
Files: 24
Total size: 39.7 KB
Directory structure:
gitextract_gearmds4/
├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── ConfigurationService.js
├── README.md
├── S3Service.js
├── app/
│ ├── IpcRendererService.js
│ ├── components/
│ │ ├── AccessForm.jsx
│ │ ├── SettingsMenu.jsx
│ │ └── StatusMenu.jsx
│ ├── containers/
│ │ └── app.jsx
│ ├── index.jsx
│ └── styles/
│ ├── base/
│ │ ├── _animations.scss
│ │ ├── _main.scss
│ │ └── _typography.scss
│ ├── components/
│ │ ├── _accessForm.scss
│ │ ├── _settingsMenu.scss
│ │ └── _statusMenu.scss
│ └── main.scss
├── index.html
├── main.js
├── package.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["es2015", "react"]
}
================================================
FILE: .eslintignore
================================================
public/
node_modules/
webpack.config.js
================================================
FILE: .eslintrc
================================================
{
"extends": "airbnb-base",
"rules": {
"import/no-extraneous-dependencies": 0,
"import/no-unresolved": 0,
"no-case-declarations": 0,
"no-param-reassign": 0,
"consistent-return": 0
}
}
================================================
FILE: .gitignore
================================================
node_modules/
.idea
public/
s3-uploader-darwin-x64/
*.log
.awscredentials.json
.DS_Store
================================================
FILE: ConfigurationService.js
================================================
const fs = require('fs');
/**
* Default file path where credentials will be stored.
* @type {string}
*/
const configFilePath = __dirname + '/.awscredentials.json';
/**
* Encoding used while reading persistent storage.
* @type {string}
*/
const defaultEncoding = 'utf-8';
/**
* Initial Configuration.
* @type {{accessKey: string, secretKey: string, bucket: string, ACL: string, storageClass: string,
* encryption: boolean}}
*/
const configuration = {
accessKey: '',
secretKey: '',
bucket: '',
folder: '',
ACL: '',
storageClass: '',
encryption: false,
};
/**
* Merges old config with provided one. Saves changes to persistent storage.
* @param newConfig
*/
const updateConfig = (newConfig) => {
Object.assign(configuration, newConfig);
fs.writeFile(configFilePath, JSON.stringify(configuration), {}, (err) => {
if (err) throw new Error(err);
});
};
/**
* Loads configuration from persistent storage if it's possible.
*/
const loadConfig = () => new Promise((resolve, reject) => {
fs.readFile(configFilePath, defaultEncoding, (err, data) => {
if (err) {
return reject(err);
}
Object.assign(configuration, JSON.parse(data));
return resolve(configuration);
});
});
/**
* Returns value for given key from configuration directory. Fast, in-memory function (fs not used).
* @param key
*/
const getItem = (key) => configuration[key];
module.exports = {
getItem,
updateConfig,
loadConfig,
};
================================================
FILE: README.md
================================================
## aws-s3-uploader
Simple macOS app for uploading files to Amazon Web Services.
Just drag & drop files you'd like to upload on bucket icon displayed on your status bar or application window.

#### Configuration
Simply click on bucket and follow instructions. You'll need access key and secret key for AWS user with S3 Exec Role.
You can also try modifying `.awscredentials.json` file on your own.
#### Features
- logging in to AWS S3 account with access + secret keys
- upload files by dragging them on status bar icon and on window
- settings permissions and storage classes for uploads
- get S3 link by clicking uploaded file from list
- support for dropping many files at once and directories
#### Todo
- [x] Automatic login on start (no need to enter credentials with every start)
- [ ] Add possibility to abort uploads
- [ ] Tests!
- [ ] Packages for distribution
- [ ] Possibility to disable notifications & automatic URL-to-clipboard write
#### Credits
Special thanks to [parkjisun](https://thenounproject.com/naripuru/), [Sergey Furtaev](https://thenounproject.com/furtaev/), [Timothy Miller](https://thenounproject.com/tmthymllr/), [Joe Mortell](https://thenounproject.com/JoeMortell/) for Icons from [nounproject.com](https://thenounproject.com/)
================================================
FILE: S3Service.js
================================================
const AWS = require('aws-sdk');
const EventEmitter = require('events');
const fileType = require('file-type');
const configService = require('./ConfigurationService');
/**
* High-level wrapper for AWS.S3 API with Promises instead of callbacks.
*/
class S3Service {
/**
* Constructor, creates S3Service object with AWS.S3 property.
*
* Constructor does not validate credentials validity.
* @param accessKey
* @param secretKey
*/
constructor(accessKey, secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
AWS.config.update({
credentials: new AWS.Credentials({
accessKeyId: accessKey,
secretAccessKey: secretKey,
}),
});
this.s3 = new AWS.S3();
}
/**
* AWS.S3.listBuckets wrapper.
* @returns {Promise}
*/
getBuckets() {
return new Promise((resolve, reject) => {
this.s3.listBuckets((err, data) => {
if (err) {
return reject(err);
}
return resolve(data);
});
});
}
/**
* AWS.S3.upload wrapper with some values preloaded from local configuration.
* Returns EventEmitter emiting following events:
* - error
* - progress
* - success
* @returns {EventEmitter}
*/
uploadFile(fileName, data) {
const uploadEventEmitter = new EventEmitter();
const mimeType = fileType(data);
this.s3.upload({
Body: data,
ContentType: (mimeType) ? mimeType.mime : 'application/octet-stream',
ACL: configService.getItem('ACL'),
Key: configService.getItem('folder') + '/' + fileName,
StorageClass: configService.getItem('storageClass'),
Bucket: configService.getItem('bucket'),
}, (err, payload) => {
if (err) uploadEventEmitter.emit('error', err);
uploadEventEmitter.emit('success', payload);
}).on('httpUploadProgress', (progress) => {
uploadEventEmitter.emit('progress', progress);
});
return uploadEventEmitter;
}
}
module.exports = S3Service;
================================================
FILE: app/IpcRendererService.js
================================================
const { ipcRenderer } = require('electron');
/**
* IPC Renderer Service
*
* Service for communicating renderer process with main Electron process.
*/
/**
* Send action via IPC to request S3 Buckets from AWS using provided credentials.
* @param accessKey
* @param secretKey
*/
const requestBuckets = (accessKey, secretKey) => new Promise((resolve, reject) => {
ipcRenderer.send('GET_BUCKETS', {
accessKey,
secretKey,
});
ipcRenderer.on('GET_BUCKETS_REPLY', (event, arg) => {
if (arg.success) return resolve(arg.data);
return reject(arg.error);
});
});
/**
* Sends files dropped on browser window to main process.
* @param files
*/
const sendDroppedFiles = (files) => {
// FileList is not an array - forEach & map are not allowed
const newFiles = [];
for (let i = 0; i < files.length; i++) {
newFiles.push(files[i].path);
}
ipcRenderer.send('DROP_FILES', {
files: newFiles,
});
};
/**
* Send action via IPC to save configuration.
* @param config
*/
const saveConfig = (config) => {
ipcRenderer.send('UPDATE_CONFIG', config);
};
/**
* Procedure which forwards upload events from IPC bus to renderer process callbacks.
* @param errorCallback
* @param successCallback
* @param progressCallback
* @param startCallback
*/
const subscribeUploadEvents = (errorCallback, successCallback, progressCallback, startCallback) => {
ipcRenderer.on('UPLOAD_SUCCESS', (event, arg) => {
successCallback(arg);
});
ipcRenderer.on('UPLOAD_ERROR', (event, arg) => {
errorCallback(arg);
});
ipcRenderer.on('UPLOAD_PROGRESS', (event, arg) => {
progressCallback(arg);
});
ipcRenderer.on('UPLOAD_START', (event, arg) => {
startCallback(arg);
});
};
module.exports = {
subscribeUploadEvents,
requestBuckets,
saveConfig,
sendDroppedFiles,
};
================================================
FILE: app/components/AccessForm.jsx
================================================
import React from 'react';
import '../styles/base/_animations.scss';
/**
* Presentational Component for displaying 'login form'.
*
* Handles validation and passes data to higher-order components/containers.
*/
class AccessForm extends React.Component {
/**
* Constructor, sets default state and binds context to used functions.
* @param props
*/
constructor(props) {
super(props);
this.state = {
accessKey: '',
secretKey: '',
};
this.handleSubmit = this._handleSubmit.bind(this);
this.handleAccessKeyChange = this._handleAccessKeyChange.bind(this);
this.handleSecretKeyChange = this._handleSecretKeyChange.bind(this);
};
/**
* React built-in function.
*
* After mounting component, tries to automatically fill out form with saved credentials.
*/
componentDidMount() {
const savedAccessKey = window.localStorage.getItem('accessKey') || '';
const savedSecretKey = window.localStorage.getItem('secretKey') || '';
document.getElementById('access_key_input').value = savedAccessKey;
document.getElementById('secret_key_input').value = savedSecretKey;
this.setState({
accessKey: savedAccessKey,
secretKey: savedSecretKey,
});
}
/**
* Procedure called when submitting form.
*
* Trims input, handles it and adds some fancy animations if something is wrong.
* @param event
* @private
*/
_handleSubmit(event) {
event.preventDefault();
const accessKey = this.state.accessKey.trim();
const secretKey = this.state.secretKey.trim();
if (accessKey !== '' && secretKey !== '') {
this.props.onCredentialsSubmitted(accessKey, secretKey);
} else {
if (accessKey === '') {
document.getElementById('access_key_input').focus();
document.getElementById('access_key_input').className = 'animated shake';
} else if (secretKey === '') {
document.getElementById('secret_key_input').focus();
document.getElementById('secret_key_input').className = 'animated shake';
}
// Diminish animation after it ends.
setTimeout(() => {
document.getElementById('access_key_input').className = '';
document.getElementById('secret_key_input').className = '';
}, 1500);
}
};
/**
* Changes this.state when user types something to access key field.
* @param event
* @private
*/
_handleAccessKeyChange(event) {
this.setState({
accessKey: event.target.value
});
};
/**
* Changes this.state when user types something to secret key field.
* @param event
* @private
*/
_handleSecretKeyChange(event) {
this.setState({
secretKey: event.target.value
});
};
/**
* React built-in function.
* @returns {XML}
*/
render() {
return (
<div className="access-form-container">
<div>
<div>
<p className="extra-margin">In order to access S3, please provide AWS Access Key and Secret Key of user with sufficient permissions</p>
<form onSubmit={this.handleSubmit}>
<input type="text"
name="access_key"
placeholder="AWS Access Key"
onChange={this.handleAccessKeyChange}
id="access_key_input"
/>
<input type="password"
name="secret_key"
placeholder="AWS Secret Key"
onChange={this.handleSecretKeyChange}
id="secret_key_input"
/>
<input
id="submitButton"
type="submit"
value="Submit"
/>
</form>
</div>
{ this.props.error !== null
? <p>{this.props.error.message}</p> : ''
}
</div>
</div>
);
};
}
AccessForm.propTypes = {
// Function forwarded from app container, called after validation
onCredentialsSubmitted: React.PropTypes.func.isRequired,
// Error data returned from container
error: React.PropTypes.object.isRequired,
};
export default AccessForm;
================================================
FILE: app/components/SettingsMenu.jsx
================================================
import React from 'react';
/**
* List of available storage options in AWS S3.
* @type {*[]}
*/
const storageOptions = [
{
displayName: 'Standard',
id: 'STANDARD',
}, {
displayName: 'Reduced Redundancy',
id: 'REDUCED_REDUNDANCY',
}, {
displayName: 'Infrequent Access',
id: 'STANDARD_IA',
},
];
/**
* List of available ACL policies in AWS S3.
* @type {*[]}
*/
const permissionsOptions = [
{
displayName: 'Private',
id: 'private',
}, {
displayName: 'Public Read',
id: 'public-read',
}, {
displayName: 'Public Read/Write',
id: 'public-read-write',
}
];
class SettingsMenu extends React.Component {
/**
* Constructor, sets default state and binds context to used functions.
* @param props
*/
constructor(props) {
super(props);
this.state = {
storageClass: '',
ACL: '',
encryption: '',
bucket: '',
folder: '',
};
this.storageOptionChange = this._storageOptionChange.bind(this);
this.permissionOptionChange = this._permissionOptionChange.bind(this);
this.encryptionOptionChange = this._encryptionOptionChange.bind(this);
this.changeDefault = this._changeDefault.bind(this);
this.checkSelection = this._checkSelection.bind(this);
this.folderChange = this._changeFolder.bind(this);
}
/**
* Changes this.state when user selects new default bucket.
* @param bucket
* @private
*/
_changeDefault(bucket) {
this.setState({
bucket,
});
}
/**
* Changes this.folder with user supplied folder path
* @param folder
* @private
*/
_changeFolder(folder) {
this.setState({
folder,
});
}
/**
* Checks if selected item is currently selected.
* @param item
* @param selection
* @param defaultClasses
* @private
*/
_checkSelection(item, selection, defaultClasses = '') {
if(item === selection) {
return 'selected ' + defaultClasses;
} else {
return defaultClasses;
}
}
/**
* Changes this.state when user changes storage class.
* @param event
* @param storageClass
* @private
*/
_storageOptionChange(event, storageClass) {
event.preventDefault();
this.setState({
storageClass,
});
}
/**
* Changes this.state when user changes default upload permissions
* @param event
* @param ACL
* @private
*/
_permissionOptionChange(event, ACL) {
event.preventDefault();
this.setState({
ACL,
});
}
/**
* Changes this.state when user toggles encryption
* @param event
* @private
*/
_encryptionOptionChange(event) {
this.setState({
encryption: event.target.checked ? 'AES256' : '',
});
}
/**
* React built-in function
* @returns {XML}
*/
render() {
return (
<div className="permissions-dialog-container">
<div className="left column">
<ul>
{this.props.buckets.map((bucket, index) =>
<li key={index}>
<div onClick={(e) => this.changeDefault(bucket.Name)} id={index}>
<span className={this.checkSelection(bucket.Name, this.state.bucket)}>
{bucket.Name}
</span>
</div>
</li>
)}
</ul>
</div>
<div className="right column">
<form>
<p className="form-head">Folder</p>
<input type="text" name="folder"
onBlur={(e) => this.folderChange(e.target.value)}>
</input>
</form>
<form>
<p className="form-head">Storage Class</p>
{
storageOptions.map((option, index) =>
<button type="radio" name="storage"
onClick={(e) => this.storageOptionChange(e, option.id)} key={index}>
<span className={this.checkSelection(option.id, this.state.storageClass, 'white big')}>
{option.displayName}
</span>
</button>
)
}
</form>
<form>
<p className="form-head">Default Permissions</p>
{
permissionsOptions.map((option, index) =>
<button type="radio" name="permissions"
onClick={(e) => this.permissionOptionChange(e, option.id)} key={index}>
<span className={this.checkSelection(option.id, this.state.ACL, 'white big')}>
{option.displayName}
</span>
</button>
)
}
</form>
<button onClick={(e) => this.props.onSettingsSelected(this.state)}>
Confirm
</button>
</div>
</div>
);
}
}
SettingsMenu.propTypes = {
// Function forwarded from app container, to be called when confirming settings
onSettingsSelected: React.PropTypes.func.isRequired,
// List of buckets returned from S3Service
buckets: React.PropTypes.array.isRequired,
};
export default SettingsMenu;
================================================
FILE: app/components/StatusMenu.jsx
================================================
import React from 'react';
const { clipboard } = require('electron');
/**
* Changes clipboard contents to file URL from AWS S3 if file was uploaded successfully.
* @param file
*/
const saveLinkToClipboard = (file) => {
if (file.status === 'uploaded' && file.url !== '') {
clipboard.writeText(file.url);
} else {
console.warn('File has been not uploaded yet!');
}
};
/**
* Presentational component showing all processed files statuses and current mode.
*/
const StatusMenu = (props) => (
<div className="status-menu-container">
<div className="status-menu-bar">
<div className="align-start">
<span className="bigger">{props.bucket}</span>
<span>Folder: {props.folder}</span>
<span>Permissions: {props.ACL}</span>
<span>Storage Class: {props.storageClass}</span>
</div>
<div className="settings" onClick={(e) => props.resetSettings()}></div>
</div>
{
props.files.length === 0
?
<div className="status-menu-nofiles">
<span>No files were uploaded yet.</span>
</div>
:
<ul className="status-menu-filelist">
{
props.files.reverse().map((file, index) =>
<li key={index} onClick={(e) => saveLinkToClipboard(file)}>
<span>{file.key.split('/').pop()}</span>
<div className={file.status + ' status-icon'}/>
</li>
)
}
</ul>
}
</div>
);
StatusMenu.propTypes = {
// Current permissions e.g. public-read-write
ACL: React.PropTypes.string.isRequired,
// Bucket where files fill be uploaded
bucket: React.PropTypes.string.isRequired,
// Folder (Prefix) to where the file will be uploaded.
folder: React.PropTypes.string.isRequired,
// Array of all files (uploaded, failed, in progress)
files: React.PropTypes.arrayOf(React.PropTypes.shape({
// Filename
key: React.PropTypes.string.isRequired,
// Absolute path to file in local filesystem
path: React.PropTypes.string.isRequired,
// File status
status: React.PropTypes.oneOf(['uploaded', 'uploading', 'failed']),
// URL to file stored in AWS S3 service, will be set to '' if file is not uploaded
url: React.PropTypes.string.isRequired,
}).isRequired).isRequired,
// Current storage class mode e.g. STANDARD_IA (Infrequent Access)
storageClass: React.PropTypes.string.isRequired,
// Function invoked when user wants to reset settings
resetSettings: React.PropTypes.func.isRequired,
};
export default StatusMenu;
================================================
FILE: app/containers/app.jsx
================================================
import React from 'react';
import AccessForm from '../components/AccessForm.jsx';
import StatusMenu from '../components/StatusMenu.jsx';
import SettingsMenu from '../components/SettingsMenu.jsx';
import IpcService from '../IpcRendererService';
import '../styles/main.scss';
/**
* Main Application react container responsible for front-end logic.
* Subscribes to IPCRendererService for main process events, displays presentational components, saves credentials and more.
* Due to lack of Redux, every information is stored in this.state.
*/
class Application extends React.Component {
/**
* constructor
* @param {object} props
*/
constructor(props) {
super(props);
this.state = {
// Default permissions e.g. public-read-write
ACL: window.localStorage.getItem('ACL') || false,
// Default bucket name
bucket: window.localStorage.getItem('bucket') || '',
// Default folder name
folder: window.localStorage.getItem('folder') || '',
// Available buckets array
buckets: [],
// List of files which are being uploaded and were already uploaded
files: [],
// Tells whether user has entered correct AWS credentials
isLoggedIn: window.localStorage.getItem('isSetupCorrectly') || false,
// Set to true during S3.listBuckets
isLoading: false,
// Set to true after confirming settings in StatusMenu component
isSettingsSet: window.localStorage.getItem('isSetupCorrectly') || false,
// Contains error information if applicable
loginError: {},
// Default storage class, e.g. REDUCED_REDUNDANCY
storageClass: window.localStorage.getItem('storageClass') || '',
};
this.bucketsLoaded = this._bucketsLoaded.bind(this);
this.credentialsSubmitted = this._credentialsSubmitted.bind(this);
this.getMenu = this._getMenu.bind(this);
this.resetSettings = this._resetSettings.bind(this);
this.settingsSet = this._settingsSet.bind(this);
this.startLoading = this._startLoading.bind(this);
this.uploadStarted = this._uploadStarted.bind(this);
this.uploadFailed = this._uploadFailed.bind(this);
this.uploadSucceeded = this._uploadSucceeded.bind(this);
this.uploadProgressed = this._uploadProgressed.bind(this);
/**
* Subscribe for events from Electron main process related with uploading files.
*/
IpcService.subscribeUploadEvents(
this.uploadFailed,
this.uploadSucceeded,
this.uploadProgressed,
this.uploadStarted
);
/**
* Add possibility to drag files on window and prevent loading content of it.
*/
window.addEventListener('dragover', (event) => {
event.preventDefault();
});
window.addEventListener('drop', (event) => {
event.preventDefault();
IpcService.sendDroppedFiles(event.dataTransfer.files);
});
}
/**
* Procedure fired when bucket list is fetched using AWS SDK.
* @param payload
* @private
*/
_bucketsLoaded(payload) {
this.setState({
buckets: payload.Buckets,
isLoggedIn: true,
loginError: {},
isLoading: false,
});
}
/**
* Procedure fired when user enters credentials in AccessForm component. Calls AWS.S3.listBuckets
* function with provided keys.
* @param accessKey
* @param secretKey
* @private
*/
_credentialsSubmitted(accessKey, secretKey) {
this.startLoading();
IpcService.requestBuckets(accessKey, secretKey)
.then((data) => {
this.bucketsLoaded(data);
window.localStorage.setItem('accessKey', accessKey);
window.localStorage.setItem('secretKey', secretKey);
})
.catch((loginError) => {
this.setState({
loginError,
isLoggedIn: false,
isLoading: false,
})
});
}
/**
* Rendering helper function, returns presentational component suitable for current app status.
* E.g. After entering credentials and before S3 API response, loading indicator is displayed.
* @returns {XML}
* @private
*/
_getMenu() {
if (this.state.isLoading) {
return <div className="spin-box"></div>;
}
if (this.state.isLoggedIn) {
if (this.state.isSettingsSet) {
return <StatusMenu ACL={this.state.ACL}
bucket={this.state.bucket}
folder={this.state.folder}
files={this.state.files}
resetSettings={this.resetSettings}
storageClass={this.state.storageClass}/>;
} else {
return <SettingsMenu onSettingsSelected={this.settingsSet}
buckets={this.state.buckets}
ACL={this.state.ACL} />;
}
} else {
return <AccessForm onCredentialsSubmitted={this.credentialsSubmitted}
error={this.state.loginError} />
}
}
_resetSettings() {
window.localStorage.setItem('isSetupCorrectly', false);
this.setState({
isLoggedIn: false,
isSettingsSet: false,
});
}
/**
* Procedure fired after saving settings in SettingsMenu presentational component.
* Saves configuration in localStorage and current state. Also sends this information to main
* electron process.
*
* @param settings
* @private
*/
_settingsSet(settings) {
window.localStorage.setItem('storageClass', settings.storageClass);
window.localStorage.setItem('ACL', settings.ACL);
window.localStorage.setItem('encryption', settings.encryption);
window.localStorage.setItem('bucket', settings.bucket);
window.localStorage.setItem('folder', settings.folder);
window.localStorage.setItem('isSetupCorrectly', true);
IpcService.saveConfig({
ACL: settings.ACL,
storageClass: settings.storageClass,
encryption: settings.encryption,
bucket: settings.bucket,
folder: settings.folder,
});
this.setState({
ACL: settings.ACL,
bucket: settings.bucket,
folder: settings.folder,
isSettingsSet: true,
storageClass: settings.storageClass,
});
}
/**
* Shows loading indicator
* @private
*/
_startLoading() {
this.setState({
isLoading: true
});
}
// TODO: Add directories support and abort function
/**
* Procedure fired when Electron main process notifies renderer process via IPC about upload process initiation.
* Appends this.state.files with dropped files.
* Contains file path dropped on icon.
*
* @param file
* @private
*/
_uploadStarted(file) {
const files = this.state.files;
const folder = this.state.folder;
files.push({
path: file.data,
key: folder + '/' + file.data.split('/').pop(),
status: 'uploading',
url: '',
});
this.setState({
files
});
}
/**
* Procedure fired when Electron main process notifies renderer process via IPC about error during upload.
* Contains error information.
*
* @param error
* @private
*/
_uploadFailed(error) {
const newFiles = this.state.files;
let updated = this.state.files.find((file) => file.key === error.data.Key);
updated.status = 'failed';
updated.error = error;
this.setState({
files: newFiles,
});
}
/**
* Procedure fired when Electron main process notifies renderer process via IPC about successful upload.
* @param data
* @private
*/
_uploadSucceeded(data) {
const newFiles = this.state.files;
let updated = this.state.files.find((file) => file.key === data.data.Key);
updated.url = data.data.Location;
updated.status = 'uploaded';
this.setState({
files: newFiles.reverse(),
});
}
/**
* Procedure fired when Electron main process notifies renderer process via IPC about progression in uploading a file(s).
* @param progress
* @private
*/
_uploadProgressed(progress) {
}
/**
* React built-in function
* @returns {XML}
*/
render() {
return (
<div>
{this.getMenu()}
</div>
);
}
}
export default Application;
================================================
FILE: app/index.jsx
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import Application from './containers/app.jsx';
/**
* Renders Application Container into DOM
*/
ReactDOM.render(<Application />, document.getElementById('app'));
================================================
FILE: app/styles/base/_animations.scss
================================================
// Wrong input animation
@keyframes shake {
from, to {
transform: translate3d(0, 0, 0);
}
10%, 30%, 50%, 70%, 90% {
transform: translate3d(-10px, 0, 0);
}
20%, 40%, 60%, 80% {
transform: translate3d(10px, 0, 0);
}
}
.shake {
animation-name: shake;
}
.animated {
animation-duration: 1s;
animation-fill-mode: both;
}
.animated.infinite {
animation-iteration-count: infinite;
}
.animated.hinge {
animation-duration: 2s;
}
.animated.flipOutX,
.animated.flipOutY,
.animated.bounceIn,
.animated.bounceOut {
animation-duration: .75s;
}
// Loading Animation
$boxSize: 15;
$color: #dfdedd;
$color2: #4f4d49;
.spin-box {
position: absolute;
margin: auto;
left: 0;
top: 0;
bottom: 0;
right: 0;
border-radius: 100%;
width: $boxSize * 1px;
height: $boxSize * 1px;
box-shadow:
$boxSize*1px $boxSize*1px $color2,
$boxSize*-1px $boxSize*1px $color,
$boxSize*-1px $boxSize*-1px $color2,
$boxSize*1px $boxSize*-1px $color;
animation: spin ease infinite 4s;
}
@keyframes spin {
0%, 100% {
box-shadow:
$boxSize*1px $boxSize*1px $color2,
$boxSize*-1px $boxSize*1px $color,
$boxSize*-1px $boxSize*-1px $color2,
$boxSize*1px $boxSize*-1px $color;
}
25% {
box-shadow:
$boxSize*-1px $boxSize*1px $color,
$boxSize*-1px $boxSize*-1px $color2,
$boxSize*1px $boxSize*-1px $color,
$boxSize*1px $boxSize*1px $color2;
}
50% {
box-shadow:
$boxSize*-1px $boxSize*-1px $color2,
$boxSize*1px $boxSize*-1px $color,
$boxSize*1px $boxSize*1px $color2,
$boxSize*-1px $boxSize*1px $color;
}
75% {
box-shadow:
$boxSize*1px $boxSize*-1px $color,
$boxSize*1px $boxSize*1px $color2,
$boxSize*-1px $boxSize*1px $color,
$boxSize*-1px $boxSize*-1px $color2;
}
}
================================================
FILE: app/styles/base/_main.scss
================================================
$white-color: #EAEAEA;
$grey-color: #b4b4b4;
$black-color: #424242;
$green-color: #2dc83a;
html {
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body, #app {
height: 100%;
margin: 0;
}
button {
background: none;
border: none;
color: #FFFFFF;
font-size: 1.2em;
font-weight: 400;
}
::-webkit-scrollbar {
background: transparent;
width: 0;
}
*:focus {
outline: none;
}
.white {
color: $white-color;
}
.extra-margin {
margin: 10px;
}
================================================
FILE: app/styles/base/_typography.scss
================================================
@import './_main.scss';
p, span, form, input, button {
color: $black-color;
font-family: "Futura-Medium", sans-serif;
font-size: 10px;
letter-spacing: -0.30px;
line-height: 1;
margin-top: 0;
text-transform: none;
text-align: center;
}
.big {
font-size: 14px;
}
================================================
FILE: app/styles/components/_accessForm.scss
================================================
@import '../base/_main.scss';
.access-form-container {
display: flex;
flex-direction: column;
align-content: center;
align-items: center;
justify-content: center;
height: 90vh;
form {
display: flex;
flex-direction: column;
align-content: center;
align-items: center;
justify-content: center;
margin: 20px;
input {
width: 200px;
border-width: 0 0 1px 0;
}
#submitButton {
margin-top: 10px;
border-radius: 50px;
font-family: "Montserrat", sans-serif;
font-size: 1.2em;
font-weight: 700;
padding: 10px 40px 10px 40px;
border: none;
background-color: $green-color;
color: $white-color;
}
}
}
================================================
FILE: app/styles/components/_settingsMenu.scss
================================================
@import '../base/_main';
.permissions-dialog-container {
display: flex;
flex-direction: row;
.column {
width: 50%;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.left {
background: #F5F5F5;
}
.selected {
text-decoration: underline;
}
.right {
background-color: $green-color;
}
form {
margin: 10px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
p {
margin-top: 5px;
margin-bottom: 5px;
font-size: 9px;
color: #FFFFFF;
}
}
.form-head {
opacity: 0.7;
}
ul {
padding: 0;
margin: auto;
}
li {
list-style: none;
overflow: hidden;
white-space: nowrap;
}
}
================================================
FILE: app/styles/components/_statusMenu.scss
================================================
@import '../base/_main';
.status-menu-container {
width: 100%;
height: 95vh;
}
.status-menu-bar {
background-color: $green-color;
display: flex;
flex-direction: row;
padding: 10px;
width: 100%;
height: 40px;
p, span {
color: $white-color;
}
div {
display: flex;
flex-direction: column;
}
.align-start {
align-items: flex-start;
}
.bigger {
font-size: 1.1em;
}
.settings {
height: 20px;
width: 20px;
background-image: url('../images/settings.png');
background-size: contain;
background-repeat: no-repeat;
top: 0;
right: 0;
margin: 10px;
position: absolute;
}
}
.status-menu-filelist {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
li {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 15px;
padding: 8px;
}
li:hover {
background-color: $grey-color;
}
.status-icon {
background-size: contain;
background-repeat: no-repeat;
height: 16px;
width: 16px;
}
.uploaded {
background-image: url('../images/okay.png');
}
.failed {
background-image: url('../images/error.png');
}
.uploading {
animation: blink 1s linear infinite;
background-color: $green-color;
border-radius: 100%;
width: 8px;
height: 8px;
}
}
.status-menu-nofiles {
align-content: center;
align-items: center;
display: flex;
height: 80%;
justify-content: center;
width: 100%;
}
@keyframes blink {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
================================================
FILE: app/styles/main.scss
================================================
// Base
@import 'base/_typography';
@import 'base/_main';
@import 'base/_animations';
// Components
@import 'components/_accessForm.scss';
@import 'components/settingsMenu';
@import 'components/_statusMenu.scss';
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>S3 Uploader</title>
</head>
<body>
<div id="app"></div>
</body>
<script>
require('./public/bundle.js')
</script>
</html>
================================================
FILE: main.js
================================================
const menubar = require('menubar');
const fs = require('fs');
const path = require('path');
const notifier = require('node-notifier');
const { ipcMain, clipboard, Menu } = require('electron');
const configService = require('./ConfigurationService');
const S3Service = require('./S3Service');
/**
* Electron window high-level wrapped constructor.
*/
const mb = menubar({
width: 400,
height: 300,
});
let s3 = null;
/**
* Sends data from main Electron process to renderer process.
* Works only if window has been instantiated first.
* @param thread
* @param data
*/
const sendWebContentsMessage = (thread, data) => {
if (mb.window !== undefined) {
mb.window.webContents.send(thread, {
data,
});
} else {
console.warn('mb.window is not defined, sending message ignored...');
}
};
/**
* Sends OS-level native notification invoker
* @param title
* @param message
*/
const sendNotification = (title, message) => {
notifier.notify({
title,
message,
});
};
/**
* S3.ManagedUpload high-level wrapper.
*
* Forwards EventEmitter events to IPC Bus to renderer process.
* Sends notification if process fails or succeeds.
* Automatically replaces clipboard contents with URL to file in AWS S3.
*
* @param uploadEventEmitter
* @param file
*/
const handleUpload = (uploadEventEmitter, file) => {
sendWebContentsMessage('UPLOAD_START', file);
uploadEventEmitter.on('error', (error) => {
sendNotification('Upload Error!', 'Click icon for more details...');
sendWebContentsMessage('UPLOAD_ERROR', error);
});
uploadEventEmitter.on('progress', (data) => {
sendWebContentsMessage('UPLOAD_PROGRESS', data);
});
uploadEventEmitter.on('success', (data) => {
clipboard.writeText(data.Location);
sendNotification('File Uploaded!', 'Link has been copied to clipboard');
sendWebContentsMessage('UPLOAD_SUCCESS', data);
});
};
/**
* Function returns Promise resolved if supplied path is directory (contains files inside this
* directory) or rejected if it's file.
* @param file
*/
const checkForDirectory = (file) => new Promise((resolve, reject) => {
fs.stat(file, (err, stats) => {
if (err) {
throw new Error(err);
} else if (stats.isDirectory()) {
fs.readdir(file, (error, files) => {
if (err) {
throw new Error(error);
}
return resolve(files.map((node) => path.join(file, node)));
});
} else {
return reject();
}
});
});
/**
* Reads file and passes it's binary data for upload.
* @param file
*/
const readFileAndUpload = (file) => {
fs.readFile(file, (err, data) => {
if (err) throw new Error(err);
handleUpload(s3.uploadFile(file.split('/').pop(), data), file);
});
};
/**
* Callback function for handling drop-files events.
*
* Takes array of files (directories) as argument and processes sequentially for upload.
*
* Fails if S3Service has been not initialized yet.
* @param files
*/
const handleFiles = (files) => {
if (s3 !== null) {
files.forEach((file) => {
checkForDirectory(file)
.then((dirFiles) => handleFiles(dirFiles))
.catch(() => readFileAndUpload(file));
});
} else {
throw new Error('Client has been not initialized yet!');
}
};
/**
* MenuBar EventEmitter listener.
*
* Listens for window readiness and loads config from file.
* If config is present, instantiates S3Service basing on that information.
*/
mb.on('ready', () => {
const template = [{label: 'Actions', submenu: [{role: "paste"}, {role: "quit"}]}];
const menu = Menu.buildFromTemplate(template);
//Menu.setApplicationMenu(menu);
configService.loadConfig()
.then((config) => {
if (config.bucket !== null) {
s3 = new S3Service(config.accessKey, config.secretKey);
} else {
throw new Error('Not ready for uploading, please configure tool first.');
}
})
.catch(() => {
throw new Error('Error while loading configuration');
});
mb.tray.on('drop-files', (event, files) => handleFiles(files));
});
/**
* IPC EventEmitter listener.
* Receiver of events from renderer process.
*/
ipcMain.on('GET_BUCKETS', (event, arg) => {
s3 = new S3Service(arg.accessKey, arg.secretKey);
s3.getBuckets().then((data) => {
event.sender.send('GET_BUCKETS_REPLY', {
success: true,
data,
});
configService.updateConfig(arg);
}).catch((error) => {
event.sender.send('GET_BUCKETS_REPLY', {
success: false,
error,
});
});
});
ipcMain.on('UPDATE_CONFIG', (event, arg) => {
configService.updateConfig(arg);
});
ipcMain.on('DROP_FILES', (event, arg) => {
handleFiles(arg.files);
});
================================================
FILE: package.json
================================================
{
"name": "s3-uploader",
"version": "0.0.2",
"description": "AWS S3 Uploader ",
"main": "main.js",
"scripts": {
"start": "webpack -w & electron .",
"clean": "rm -fr node_modules && npm install",
"eslint": "eslint .",
"prod": "npm prune --production",
"package-osx": "npm run prod && electron-packager . --platform=darwin --arch=x64",
"package-win": "npm run prod && electron packager . --platform=win --arch=x64 --asar",
"package-linux": "npm run prod && electron-packager . --platform=linux --arch=x64 --asar"
},
"keywords": [
"Electron",
"S3",
"Amazon Web Services",
"React"
],
"author": "Rafal Wilinski",
"repository": {
"type": "git",
"url": "https://github.com/RafalWilinski/s3-uploader.git"
},
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.10.1",
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.11.1",
"css-loader": "^0.23.1",
"devtron": "^1.2.1",
"electron-debug": "^1.0.1",
"electron-packager": "^7.3.0",
"eslint": "^3.0.1",
"eslint-config-airbnb-base": "^4.0.0",
"eslint-plugin-import": "^1.10.2",
"node-sass": "^3.8.0",
"sass-loader": "^4.0.0",
"spectron": "^3.3.0",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"file-loader":"^0.9.0",
"webpack": "^1.13.1"
},
"dependencies": {
"aws-sdk": "^2.4.4",
"electron-prebuilt": "^1.2.0",
"electron-packager": "^7.3.0",
"menubar": "^4.1.2",
"node-notifier": "^4.6.0",
"react": "^15.2.0",
"react-dom": "^15.2.0",
"request": "^2.72.0",
"file-type": "^3.9.0"
}
}
================================================
FILE: webpack.config.js
================================================
const webpack = require('webpack');
module.exports = {
entry: [
'./app/index.jsx',
],
output: {
filename: 'public/bundle.js',
},
target: 'electron',
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules)/,
loader: 'babel',
query: {
presets: ['react', 'es2015'],
},
}, {
test: /\.scss?$/,
exclude: /(node_modules)/,
loader: 'style!css!sass',
}, {
test: /\.(jpg|png)$/,
loader: 'url?limit=25000',
include: /(app)/,
}
],
noParse: [
/aws\-sdk/,
]
},
watchOptions: {
poll: 100,
},
plugins: [
new webpack.ExternalsPlugin('commonjs', [
'desktop-capturer',
'electron-prebuilt',
'electron',
'ipc',
'ipc-renderer',
'native-image',
'remote',
'web-frame',
'clipboard',
'crash-reporter',
'screen',
'shell'
])
]
};
gitextract_gearmds4/ ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── ConfigurationService.js ├── README.md ├── S3Service.js ├── app/ │ ├── IpcRendererService.js │ ├── components/ │ │ ├── AccessForm.jsx │ │ ├── SettingsMenu.jsx │ │ └── StatusMenu.jsx │ ├── containers/ │ │ └── app.jsx │ ├── index.jsx │ └── styles/ │ ├── base/ │ │ ├── _animations.scss │ │ ├── _main.scss │ │ └── _typography.scss │ ├── components/ │ │ ├── _accessForm.scss │ │ ├── _settingsMenu.scss │ │ └── _statusMenu.scss │ └── main.scss ├── index.html ├── main.js ├── package.json └── webpack.config.js
SYMBOL INDEX (34 symbols across 4 files)
FILE: S3Service.js
constant AWS (line 1) | const AWS = require('aws-sdk');
class S3Service (line 9) | class S3Service {
method constructor (line 17) | constructor(accessKey, secretKey) {
method getBuckets (line 35) | getBuckets() {
method uploadFile (line 54) | uploadFile(fileName, data) {
FILE: app/components/AccessForm.jsx
class AccessForm (line 10) | class AccessForm extends React.Component {
method constructor (line 16) | constructor(props) {
method componentDidMount (line 34) | componentDidMount() {
method _handleSubmit (line 53) | _handleSubmit(event) {
method _handleAccessKeyChange (line 82) | _handleAccessKeyChange(event) {
method _handleSecretKeyChange (line 93) | _handleSecretKeyChange(event) {
method render (line 103) | render() {
FILE: app/components/SettingsMenu.jsx
class SettingsMenu (line 37) | class SettingsMenu extends React.Component {
method constructor (line 43) | constructor(props) {
method _changeDefault (line 66) | _changeDefault(bucket) {
method _changeFolder (line 77) | _changeFolder(folder) {
method _checkSelection (line 90) | _checkSelection(item, selection, defaultClasses = '') {
method _storageOptionChange (line 104) | _storageOptionChange(event, storageClass) {
method _permissionOptionChange (line 117) | _permissionOptionChange(event, ACL) {
method _encryptionOptionChange (line 129) | _encryptionOptionChange(event) {
method render (line 139) | render() {
FILE: app/containers/app.jsx
class Application (line 14) | class Application extends React.Component {
method constructor (line 20) | constructor(props) {
method _bucketsLoaded (line 86) | _bucketsLoaded(payload) {
method _credentialsSubmitted (line 102) | _credentialsSubmitted(accessKey, secretKey) {
method _getMenu (line 126) | _getMenu() {
method _resetSettings (line 150) | _resetSettings() {
method _settingsSet (line 167) | _settingsSet(settings) {
method _startLoading (line 196) | _startLoading() {
method _uploadStarted (line 211) | _uploadStarted(file) {
method _uploadFailed (line 233) | _uploadFailed(error) {
method _uploadSucceeded (line 249) | _uploadSucceeded(data) {
method _uploadProgressed (line 265) | _uploadProgressed(progress) {
method render (line 273) | render() {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (44K chars).
[
{
"path": ".babelrc",
"chars": 37,
"preview": "{\n \"presets\": [\"es2015\", \"react\"]\n}\n"
},
{
"path": ".eslintignore",
"chars": 39,
"preview": "public/\nnode_modules/\nwebpack.config.js"
},
{
"path": ".eslintrc",
"chars": 209,
"preview": "{\n \"extends\": \"airbnb-base\",\n \"rules\": {\n \"import/no-extraneous-dependencies\": 0,\n \"import/no-unresolved\": 0,\n "
},
{
"path": ".gitignore",
"chars": 89,
"preview": "node_modules/\n.idea\npublic/\ns3-uploader-darwin-x64/\n*.log\n.awscredentials.json\n.DS_Store\n"
},
{
"path": "ConfigurationService.js",
"chars": 1464,
"preview": "const fs = require('fs');\n/**\n * Default file path where credentials will be stored.\n * @type {string}\n */\nconst configF"
},
{
"path": "README.md",
"chars": 1325,
"preview": "## aws-s3-uploader\nSimple macOS app for uploading files to Amazon Web Services.\n \nJust drag & drop files you'd like to u"
},
{
"path": "S3Service.js",
"chars": 1995,
"preview": "const AWS = require('aws-sdk');\nconst EventEmitter = require('events');\nconst fileType = require('file-type');\nconst con"
},
{
"path": "app/IpcRendererService.js",
"chars": 1831,
"preview": "const { ipcRenderer } = require('electron');\n\n/**\n * IPC Renderer Service\n *\n * Service for communicating renderer proce"
},
{
"path": "app/components/AccessForm.jsx",
"chars": 4156,
"preview": "import React from 'react';\n\nimport '../styles/base/_animations.scss';\n\n/**\n * Presentational Component for displaying 'l"
},
{
"path": "app/components/SettingsMenu.jsx",
"chars": 5103,
"preview": "import React from 'react';\n\n/**\n * List of available storage options in AWS S3.\n * @type {*[]}\n */\nconst storageOptions "
},
{
"path": "app/components/StatusMenu.jsx",
"chars": 2563,
"preview": "import React from 'react';\nconst { clipboard } = require('electron');\n\n/**\n * Changes clipboard contents to file URL fro"
},
{
"path": "app/containers/app.jsx",
"chars": 8155,
"preview": "import React from 'react';\nimport AccessForm from '../components/AccessForm.jsx';\nimport StatusMenu from '../components/"
},
{
"path": "app/index.jsx",
"chars": 226,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport Application from './containers/app.jsx';\n\n/**\n * Ren"
},
{
"path": "app/styles/base/_animations.scss",
"chars": 1810,
"preview": "// Wrong input animation\n@keyframes shake {\n from, to {\n transform: translate3d(0, 0, 0);\n }\n\n 10%, 30%, 50%, 70%,"
},
{
"path": "app/styles/base/_main.scss",
"chars": 521,
"preview": "$white-color: #EAEAEA;\n$grey-color: #b4b4b4;\n$black-color: #424242;\n$green-color: #2dc83a;\n\nhtml {\n overflow-x: hidden;"
},
{
"path": "app/styles/base/_typography.scss",
"chars": 280,
"preview": "@import './_main.scss';\n\np, span, form, input, button {\n color: $black-color;\n font-family: \"Futura-Medium\", sans-seri"
},
{
"path": "app/styles/components/_accessForm.scss",
"chars": 713,
"preview": "@import '../base/_main.scss';\n\n.access-form-container {\n display: flex;\n flex-direction: column;\n align-content: cent"
},
{
"path": "app/styles/components/_settingsMenu.scss",
"chars": 809,
"preview": "@import '../base/_main';\n\n.permissions-dialog-container {\n display: flex;\n flex-direction: row;\n\n .column {\n width"
},
{
"path": "app/styles/components/_statusMenu.scss",
"chars": 1600,
"preview": "@import '../base/_main';\n\n.status-menu-container {\n width: 100%;\n height: 95vh;\n}\n\n.status-menu-bar {\n background-col"
},
{
"path": "app/styles/main.scss",
"chars": 214,
"preview": "// Base\n@import 'base/_typography';\n@import 'base/_main';\n@import 'base/_animations';\n\n// Components\n@import 'components"
},
{
"path": "index.html",
"chars": 209,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <title>S3 Uploader</title>\n </head>\n <body>\n <div id"
},
{
"path": "main.js",
"chars": 4695,
"preview": "const menubar = require('menubar');\nconst fs = require('fs');\nconst path = require('path');\nconst notifier = require('no"
},
{
"path": "package.json",
"chars": 1685,
"preview": "{\n \"name\": \"s3-uploader\",\n \"version\": \"0.0.2\",\n \"description\": \"AWS S3 Uploader \",\n \"main\": \"main.js\",\n \"scripts\": "
},
{
"path": "webpack.config.js",
"chars": 971,
"preview": "const webpack = require('webpack');\n\nmodule.exports = {\n entry: [\n './app/index.jsx',\n ],\n output: {\n filename:"
}
]
About this extraction
This page contains the full source code of the RafalWilinski/s3-uploader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (39.7 KB), approximately 10.8k tokens, and a symbol index with 34 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.