Frontend Development 16 min read

Building a Cross‑Platform S3 Upload GUI with Electron and Vue

This article explains how to create a cross‑platform desktop client for uploading local resources to S3‑backed CDN using Electron, Vue, and lowdb, covering UI design, project structure, main‑renderer process communication, persistent storage, command‑line integration, packaging, and update handling.

HomeTech
HomeTech
HomeTech
Building a Cross‑Platform S3 Upload GUI with Electron and Vue

Background

The company migrated its CDN resource servers from FTP to S3 object storage and developed a command‑line tool called RapidTrans to simplify uploads. To make path selection more user‑friendly, the team decided to wrap RapidTrans in a GUI desktop client.

Client Interface

The GUI provides a visual file selector for the local path and an input field for the CDN path.

Feature Analysis

Desktop client supporting Windows and macOS.

Local path selection via file dialog or drag‑and‑drop.

CDN path entered through a text box.

Automatic mapping storage for previously used paths.

S3 configuration is configurable.

Auto‑update capability.

Overwrite upload option.

Technology Selection

Electron

Vue

LowDB

Electron Overview

Electron, developed by GitHub, combines Chromium and Node.js, allowing developers to build cross‑platform desktop applications with HTML, CSS, and JavaScript.

Vue + Electron Environment Setup

# Install [email protected] (skip if already installed)
npm install -g vue-cli
vue init simulatedgreg/electron-vue s3_upload_tool

# Install dependencies and run
cd s3_upload_tool
npm install
npm run dev

Directory Structure

├─ .electron-vue
│  ├─ webpack.main.config.js
│  ├─ webpack.renderer.config.js
│  └─ webpack.web.config.js
├─ build
│  └─ icons/
├─ dist
│  ├─ electron/
│  └─ web/
├─ node_modules/
├─ src
│  ├─ main
│  │  ├─ index.dev.js
│  │  └─ index.js
│  ├─ renderer
│  │  ├─ components/
│  │  ├─ router/
│  │  ├─ store/
│  │  ├─ App.vue
│  │  └─ main.js
│  └─ index.ejs
├─ static/
├─ .babelrc
├─ .eslintignore
├─ .eslintrc.js
├─ .gitignore
├─ package.json
└─ README.md

.electron-vue

Contains three independent Webpack configuration files for the main process, renderer process, and web build.

src/main

Holds the main‑process code that accesses Node APIs and native OS features.

src/renderer

Contains the renderer‑process code, essentially a standard Vue project.

Main Process and Renderer Process

Electron separates the GUI (main process) from the page rendering (renderer process). The main process creates BrowserWindow instances, each running its own renderer.

IPC Communication

Inter‑process communication (IPC) enables the two processes to exchange messages. The renderer uses ipcRenderer to send messages, while the main process listens with ipcMain . Replies can be sent synchronously via event.returnValue or asynchronously via event.sender.send .

// Main process
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.sender.send('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.returnValue = 'pong'
})

// Renderer process
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')

The main process can also proactively send messages using BrowserWindow.webContents.send .

// Main process
const { app, BrowserWindow } = require('electron')
function createWindow () {
  let win = new BrowserWindow({ width: 800, height: 600 })
  win.loadFile('index.html')
  win.webContents.send('main-process-message', 'ping')
}
app.on('ready', createWindow)

// Renderer process
const { ipcRenderer } = require('electron')
ipcRenderer.on('main-process-message', (event, arg) => {
  console.log(arg) // prints "ping"
})

Persistent Storage

Desktop apps need lightweight local storage; the project uses lowdb , a JSON‑based database built on Lodash, compatible with Node, browsers, and Electron.

npm install lowdb -save

Electron provides app.getPath to locate system directories for storing the database file.

const { app } = require('electron')
app.getPath('userData') // e.g., "C:\Users\
\AppData\Roaming\
"

Database initialization:

'use strict'
const DataStore = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')
const path = require('path')
const fs = require('fs-extra')
const { app, remote } = require('electron')
const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData')
if (process.type !== 'renderer' && !fs.pathExistsSync(STORE_PATH)) {
  fs.mkdirpSync(STORE_PATH)
}
const adapter = new FileSync(path.join(STORE_PATH, '/data.json'))
const db = DataStore(adapter)
db.defaults({
  project: [],
  settings: { ftp: '', s3: '' }
}).write()
module.exports = db

Running the Command‑Line Tool in the Background

Since RapidTrans is a CLI tool without a Node API, the GUI launches it via a child process and streams its output to the UI.

'use strict'
import { ipcMain } from 'electron'
import { exec } from 'child_process'
import path from 'path'
import fixPath from 'fix-path'
import { logError, logInfo, logExit } from './log'
const cmdPath = path.resolve(__static, 'lib/rapid_trans')
let workerProcess
ipcMain.on('upload', (e, {dirPath, cdnPath, isCover}) => {
  runUpload(dirPath, cdnPath, isCover)
})
function runUpload (dirPath, cdnPath, isCover) {
  let cmdStr = `node src/rapid-trans.js -s "${dirPath}" -p "${cdnPath}" -q`
  if (isCover) { cmdStr += ' -f' }
  fixPath()
  logInfo('================== 开始上传 ================== \n')
  workerProcess = exec(cmdStr, { cwd: cmdPath })
  workerProcess.stdout.on('data', data => logInfo(data))
  workerProcess.stderr.on('data', data => logError(data))
  workerProcess.on('close', code => {
    logExit(code)
    logInfo('================== 上传结束 ================== \n')
  })
}
// log.js
export function logInfo (msg) { global.mainWindow.webContents.send('logInfo', msg) }
export function logError (msg) { global.mainWindow.webContents.send('logError', msg) }
export function logExit (msg) { global.mainWindow.webContents.send('logExit', msg) }
export default { logError, logExit, logInfo }

Application Packaging

The finished app is packaged for Windows and macOS using electron-builder . The build field in package.json defines product name, output directory, icons, and publishing settings.

{
  "build": {
    "productName": "S3上传工具",
    "appId": "com.autohome.s3",
    "directories": { "output": "build" },
    "asar": false,
    "publish": [{ "provider": "generic", "url": "http://xxx.com:8003/oss" }],
    "releaseInfo": { "releaseNotes": "新版更新" },
    "files": ["dist/electron/**/*", { "from": "dist/electron/static/lib/rapid_trans/node_modules", "to": "dist/electron/static/lib/rapid_trans/node_modules" }],
    "dmg": { "contents": [{ "x": 410, "y": 150, "type": "link", "path": "/Applications" }, { "x": 130, "y": 150, "type": "file" }] },
    "mac": { "icon": "build/icons/icon.icns" },
    "win": { "icon": "build/icons/icon.ico" },
    "linux": { "icon": "build/icons" }
  }
}

Application Update Prompt

Because the app is distributed internally without App Store certification, automatic updates are not feasible on macOS. Instead, the app checks for newer versions and shows a modal dialog prompting the user to download the installer manually using electron-updater .

// Main process (main.js)
import { autoUpdater } from 'electron-updater'
autoUpdater.autoDownload = false
autoUpdater.on('update-available', info => {
  mainWindow.webContents.send('updater', info)
})
app.on('ready', () => {
  if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
})

// Renderer process (updater.js)
import { ipcRenderer, shell } from 'electron'
import { MessageBox } from 'element-ui'
ipcRenderer.on('updater', (e, info) => {
  MessageBox.alert(info.releaseNotes, `请升级${info.version}版本`, {
    confirmButtonText: '立即升级',
    showClose: false,
    closeOnClickModal: false,
    dangerouslyUseHTMLString: true,
    callback (action) {
      if (action === 'confirm') {
        shell.openExternal('http://10.168.0.49/songjinda/s3_tool/download/')
        return false
      }
    }
  })
})
cross‑platformElectronNode.jsVueDesktop ApplicationS3Lowdb
HomeTech
Written by

HomeTech

HomeTech tech sharing

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.