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.
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 devDirectory 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 -saveElectron 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 = dbRunning 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
}
}
})
})HomeTech
HomeTech tech sharing
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.