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