Skip to content

Commit ae64fef

Browse files
committed
feat: add netlify deploy support
Change-Id: I61403966bb6b3453acbdc06e6673e333e5dbfe01
1 parent 4ed068a commit ae64fef

File tree

8 files changed

+279
-24
lines changed

8 files changed

+279
-24
lines changed

src/components/Main.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,11 @@ export default class App extends Vue {
262262
263263
public publish() {
264264
const { setting } = this.site
265+
if (setting.platform === 'netlify' && !setting.netlifyAccessToken && !setting.netlifySiteId) {
266+
this.$message.error(`🙁 ${this.$t('syncWarning')}`)
267+
return false
268+
}
269+
265270
if (!setting.branch && !setting.domain && !setting.token && !setting.repository) {
266271
this.$message.error(`🙁 ${this.$t('syncWarning')}`)
267272
return false

src/interfaces/setting.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface ISetting {
2-
platform: 'github' | 'coding' | 'sftp'
2+
platform: 'github' | 'coding' | 'sftp' | 'gitee' | 'netlify'
33
domain: string
44
repository: string
55
branch: string
@@ -16,6 +16,8 @@ export interface ISetting {
1616
proxyPath: string
1717
proxyPort: string
1818
enabledProxy: 'direct' | 'proxy'
19+
netlifyAccessToken: string
20+
netlifySiteId: string
1921
[index: string]: string
2022
}
2123

src/server/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export default class App {
8484
proxyPath: '',
8585
proxyPort: '',
8686
enabledProxy: 'direct',
87+
netlifySiteId: '',
88+
netlifyAccessToken: '',
8789
},
8890
commentSetting: {
8991
showComment: false,

src/server/events/deploy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ipcMain, IpcMainEvent } from 'electron'
22
import Deploy from '../deploy'
33
import Renderer from '../renderer'
44
import SftpDeploy from '../plugins/deploys/sftp'
5+
import NetlifyDeploy from '../plugins/deploys/netlify'
56

67
export default class DeployEvents {
78
constructor(appInstance: any) {
@@ -10,6 +11,7 @@ export default class DeployEvents {
1011
const deploy = new Deploy(appInstance)
1112
const sftp = new SftpDeploy(appInstance)
1213
const renderer = new Renderer(appInstance)
14+
const netlify = new NetlifyDeploy(appInstance)
1315

1416
ipcMain.removeAllListeners('site-publish')
1517
ipcMain.removeAllListeners('site-published')
@@ -23,6 +25,7 @@ export default class DeployEvents {
2325
'coding': deploy,
2426
'gitee': deploy,
2527
'sftp': sftp,
28+
'netlify': netlify,
2629
} as any)[platform]
2730

2831
// render
@@ -40,6 +43,7 @@ export default class DeployEvents {
4043
'coding': deploy,
4144
'gitee': deploy,
4245
'sftp': sftp,
46+
'netlify': netlify,
4347
} as any)[platform]
4448

4549
const result = await client.remoteDetect()
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import axios from 'axios'
4+
import normalizePath from 'normalize-path'
5+
import crypto from 'crypto'
6+
import util from 'util'
7+
import Model from '../../model'
8+
import { IApplication } from '../../interfaces/application'
9+
10+
const asyncReadFile = util.promisify(fs.readFile)
11+
12+
export default class NetlifyApi extends Model {
13+
private apiUrl: string
14+
15+
private accessToken: string
16+
17+
private siteId: string
18+
19+
private inputDir: string
20+
21+
constructor(appInstance: IApplication) {
22+
super(appInstance)
23+
this.apiUrl = 'https://api.netlify.com/api/v1/'
24+
this.accessToken = appInstance.db.setting.netlifyAccessToken
25+
this.siteId = appInstance.db.setting.netlifySiteId
26+
this.inputDir = appInstance.buildDir
27+
}
28+
29+
async request(method: 'GET' | 'PUT' | 'POST', endpoint: string, data?: any) {
30+
const endpointUrl = this.apiUrl + endpoint.replace(':site_id', this.siteId)
31+
const { setting } = this.db
32+
const proxy = setting.enabledProxy ? {
33+
host: setting.proxyPath,
34+
port: Number(setting.proxyPort),
35+
} : undefined
36+
37+
return axios(
38+
endpointUrl,
39+
{
40+
method,
41+
headers: {
42+
'User-Agent': 'Gridea',
43+
'Authorization': `Bearer ${this.accessToken}`,
44+
},
45+
data,
46+
proxy,
47+
},
48+
)
49+
}
50+
51+
async remoteDetect() {
52+
try {
53+
const res = await this.request('GET', 'sites/:site_id/')
54+
if (res.status === 200) {
55+
return {
56+
success: true,
57+
message: res.data,
58+
}
59+
}
60+
61+
return {
62+
success: false,
63+
message: res.data,
64+
}
65+
} catch (e) {
66+
return {
67+
success: false,
68+
message: e,
69+
}
70+
}
71+
}
72+
73+
async publish() {
74+
const result = {
75+
success: true,
76+
message: '同步成功',
77+
data: null,
78+
}
79+
80+
try {
81+
const localFilesList = await this.prepareLocalFilesList()
82+
const deployData = await this.request('POST', 'sites/:site_id/deploys', localFilesList)
83+
const deployId = deployData.data.id
84+
const hashOfFilesToUpload = deployData.data.required
85+
const filesToUpload = this.getFilesToUpload(localFilesList, hashOfFilesToUpload)
86+
87+
for (let i = 0; i < filesToUpload.length; i += 1) {
88+
const filePath = filesToUpload[i]
89+
90+
try {
91+
// eslint-disable-next-line no-await-in-loop
92+
const res = await this.uploadFile(filePath, deployId)
93+
94+
if (res.status === 422) {
95+
return Promise.reject(res)
96+
}
97+
} catch (e) {
98+
try {
99+
// eslint-disable-next-line no-await-in-loop
100+
const res = await this.uploadFile(filePath, deployId)
101+
102+
if (res.status === 422) {
103+
return Promise.reject(res)
104+
}
105+
} catch (error) {
106+
return Promise.reject(error)
107+
}
108+
}
109+
}
110+
111+
return result
112+
} catch (e) {
113+
result.success = false
114+
result.message = `[Server] 同步失败: ${e.message}`
115+
}
116+
}
117+
118+
async prepareLocalFilesList() {
119+
const tempFileList: any = this.readDirRecursiveSync(this.inputDir)
120+
const fileList: any = {}
121+
122+
for (const filePath of tempFileList) {
123+
if (fs.lstatSync(path.join(this.inputDir, filePath)).isDirectory()) {
124+
continue
125+
}
126+
127+
// eslint-disable-next-line no-await-in-loop
128+
const fileHash = await this.getFileHash(path.join(this.inputDir, filePath))
129+
const fileKey = `/${filePath}`.replace(/\/\//gmi, '/')
130+
fileList[fileKey] = fileHash
131+
}
132+
133+
return Promise.resolve({ files: fileList })
134+
}
135+
136+
readDirRecursiveSync(dir: string, fileList?: any) {
137+
const files = fs.readdirSync(dir)
138+
fileList = fileList || []
139+
140+
files.forEach((file) => {
141+
if (this.fileIsDirectory(dir, file)) {
142+
fileList = this.readDirRecursiveSync(path.join(dir, file), fileList)
143+
return
144+
}
145+
146+
if (this.fileIsNotExcluded(file)) {
147+
fileList.push(this.getFilePath(dir, file))
148+
}
149+
})
150+
151+
return fileList
152+
}
153+
154+
fileIsDirectory(dir: string, file: string) {
155+
return fs.statSync(path.join(dir, file)).isDirectory()
156+
}
157+
158+
fileIsNotExcluded(file: string) {
159+
return file.indexOf('.') !== 0 || file === '.htaccess' || file === '_redirects'
160+
}
161+
162+
getFilePath(dir: string, file: string, includeInputDir = false) {
163+
if (!includeInputDir) {
164+
dir = dir.replace(this.inputDir, '')
165+
}
166+
167+
return normalizePath(path.join(dir, file))
168+
}
169+
170+
getFileHash(fileName: string) {
171+
return new Promise((resolve, reject) => {
172+
const shaSumCalculator = crypto.createHash('sha1')
173+
174+
try {
175+
const fileStream = fs.createReadStream(fileName)
176+
fileStream.on('data', fileContentChunk => shaSumCalculator.update(fileContentChunk))
177+
fileStream.on('end', () => resolve(shaSumCalculator.digest('hex')))
178+
} catch (e) {
179+
return reject(e)
180+
}
181+
})
182+
}
183+
184+
getFilesToUpload(filesList: any, hashesToUpload: any) {
185+
const filePaths = Object.keys(filesList.files)
186+
const filesToUpload = []
187+
const foundedHashes = []
188+
189+
for (let i = 0; i < filePaths.length; i++) {
190+
const filePath = filePaths[i]
191+
192+
if (hashesToUpload.indexOf(filesList.files[filePath]) > -1) {
193+
filesToUpload.push(filePath.replace(/\/\//gmi, '/'))
194+
foundedHashes.push(filesList.files[filePath])
195+
}
196+
}
197+
198+
return filesToUpload
199+
}
200+
201+
async uploadFile(filePath: any, deployID: any) {
202+
const endpointUrl = `${this.apiUrl}deploys/${deployID}/files${filePath}`
203+
const fullFilePath = this.getFilePath(this.inputDir, filePath, true)
204+
const fileContent = await asyncReadFile(fullFilePath)
205+
206+
return axios(endpointUrl, {
207+
method: 'PUT',
208+
headers: {
209+
'User-Agent': 'Gridea',
210+
'Content-Type': 'application/octet-stream',
211+
'Authorization': `Bearer ${this.accessToken}`,
212+
},
213+
data: fileContent,
214+
})
215+
}
216+
}

src/server/renderer.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -506,28 +506,32 @@ export default class Renderer extends Model {
506506
fse.ensureDirSync(cssFolderPath)
507507

508508
const lessString = fs.readFileSync(lessFilePath, 'utf8')
509-
less.render(lessString, { filename: lessFilePath }, async (err: any, cssString: Less.RenderOutput) => {
510-
if (err) {
511-
console.log(err)
512-
}
513-
let { css } = cssString
514-
515-
// if have override
516-
const customConfig = this.db.themeCustomConfig
517-
const currentThemePath = urlJoin(this.appDir, 'themes', this.db.themeConfig.themeName)
518-
519-
const styleOverridePath = urlJoin(currentThemePath, 'style-override.js')
520-
const existOverrideFile = await fse.pathExists(styleOverridePath)
521-
if (existOverrideFile) {
522-
// clean cache
523-
delete __non_webpack_require__.cache[__non_webpack_require__.resolve(styleOverridePath)]
524-
525-
const generateOverride = __non_webpack_require__(styleOverridePath)
526-
const customCss = generateOverride(customConfig)
527-
css += customCss
528-
}
529-
530-
fs.writeFileSync(urlJoin(cssFolderPath, 'main.css'), css)
509+
return new Promise((resolve, reject) => {
510+
less.render(lessString, { filename: lessFilePath }, async (err: any, cssString: Less.RenderOutput) => {
511+
if (err) {
512+
console.log(err)
513+
reject(err)
514+
}
515+
let { css } = cssString
516+
517+
// if have override
518+
const customConfig = this.db.themeCustomConfig
519+
const currentThemePath = urlJoin(this.appDir, 'themes', this.db.themeConfig.themeName)
520+
521+
const styleOverridePath = urlJoin(currentThemePath, 'style-override.js')
522+
const existOverrideFile = await fse.pathExists(styleOverridePath)
523+
if (existOverrideFile) {
524+
// clean cache
525+
delete __non_webpack_require__.cache[__non_webpack_require__.resolve(styleOverridePath)]
526+
527+
const generateOverride = __non_webpack_require__(styleOverridePath)
528+
const customCss = generateOverride(customConfig)
529+
css += customCss
530+
}
531+
532+
fs.writeFileSync(urlJoin(cssFolderPath, 'main.css'), css)
533+
resolve(true)
534+
})
531535
})
532536
}
533537

src/store/modules/site.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ const siteState: Site = {
6565
proxyPath: '',
6666
proxyPort: '',
6767
enabledProxy: 'direct',
68+
netlifySiteId: '',
69+
netlifyAccessToken: '',
6870
},
6971
commentSetting: {
7072
showComment: false,

src/views/setting/includes/BasicSetting.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<a-form-item :label="$t('platform')" :labelCol="formLayout.label" :wrapperCol="formLayout.wrapper" :colon="false">
55
<a-radio-group name="platform" v-model="form.platform">
66
<a-radio value="github">Github Pages</a-radio>
7+
<a-radio value="netlify">Netlify</a-radio>
78
<a-radio value="coding">Coding Pages</a-radio>
89
<a-radio value="gitee">Gitee Pages</a-radio>
910
<a-radio value="sftp">SFTP</a-radio>
@@ -18,6 +19,19 @@
1819
<a-input v-model="form.domain" placeholder="mydomain.com" style="width: calc(100% - 96px);" />
1920
</a-input-group>
2021
</a-form-item>
22+
<template v-if="['netlify'].includes(form.platform)">
23+
<a-form-item label="Site ID" :labelCol="formLayout.label" :wrapperCol="formLayout.wrapper" :colon="false">
24+
<a-input v-model="form.netlifySiteId" />
25+
</a-form-item>
26+
<a-form-item label="Access Token" :labelCol="formLayout.label" :wrapperCol="formLayout.wrapper" :colon="false" v-if="remoteType === 'password'">
27+
<a-input v-model="form.netlifyAccessToken" :type="passVisible ? '' : 'password'">
28+
<a-icon class="icon" slot="addonAfter" :type="passVisible ? 'eye-invisible' : 'eye'" @click="passVisible = !passVisible" />
29+
</a-input>
30+
</a-form-item>
31+
<a-form-item label=" " :labelCol="formLayout.label" :wrapperCol="formLayout.wrapper" :colon="false">
32+
<a href="https://gridea.dev/netlify" target="_blank">如何配置?</a>
33+
</a-form-item>
34+
</template>
2135
<template v-if="['github', 'coding', 'gitee'].includes(form.platform)">
2236
<a-form-item :label="$t('repository')" :labelCol="formLayout.label" :wrapperCol="formLayout.wrapper" :colon="false">
2337
<a-input v-model="form.repository" />
@@ -142,6 +156,8 @@ export default class BasicSetting extends Vue {
142156
proxyPath: '',
143157
proxyPort: '',
144158
enabledProxy: 'direct',
159+
netlifyAccessToken: '',
160+
netlifySiteId: '',
145161
}
146162
147163
remoteType = 'password'
@@ -162,7 +178,11 @@ export default class BasicSetting extends Vue {
162178
&& form.remotePath
163179
&& (form.password || form.privateKey)
164180
165-
return pagesPlatfomValid || sftpPlatformValid
181+
const netlifyPlatformValid = ['netlify'].includes(form.platform)
182+
&& form.netlifyAccessToken
183+
&& form.netlifySiteId
184+
185+
return pagesPlatfomValid || sftpPlatformValid || netlifyPlatformValid
166186
}
167187
168188
mounted() {

0 commit comments

Comments
 (0)