Merge remote-tracking branch 'origin/restructure' into anmol-restructure
This commit is contained in:
@ -6,12 +6,17 @@ import { HttpCode, HttpError } from "../../common/http"
|
||||
import { Options } from "../../common/util"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { ApiHttpProvider } from "./api"
|
||||
import { UpdateHttpProvider } from "./update"
|
||||
|
||||
/**
|
||||
* Top-level and fallback HTTP provider.
|
||||
*/
|
||||
export class MainHttpProvider extends HttpProvider {
|
||||
public constructor(options: HttpProviderOptions, private readonly api: ApiHttpProvider) {
|
||||
public constructor(
|
||||
options: HttpProviderOptions,
|
||||
private readonly api: ApiHttpProvider,
|
||||
private readonly update: UpdateHttpProvider,
|
||||
) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
@ -77,13 +82,14 @@ export class MainHttpProvider extends HttpProvider {
|
||||
response.content = response.content
|
||||
.replace(/{{COMMIT}}/g, this.options.commit)
|
||||
.replace(/{{BASE}}/g, this.base(route))
|
||||
.replace(/{{APP_LIST:RUNNING}}/g, this.getAppRows(recent.running))
|
||||
.replace(/{{UPDATE:NAME}}/, await this.getUpdate())
|
||||
.replace(/{{APP_LIST:RUNNING}}/, this.getAppRows(recent.running))
|
||||
.replace(
|
||||
/{{APP_LIST:EDITORS}}/g,
|
||||
/{{APP_LIST:EDITORS}}/,
|
||||
this.getAppRows(apps.filter((app) => app.categories && app.categories.includes("Editor"))),
|
||||
)
|
||||
.replace(
|
||||
/{{APP_LIST:OTHER}}/g,
|
||||
/{{APP_LIST:OTHER}}/,
|
||||
this.getAppRows(apps.filter((app) => !app.categories || !app.categories.includes("Editor"))),
|
||||
)
|
||||
return response
|
||||
@ -94,8 +100,8 @@ export class MainHttpProvider extends HttpProvider {
|
||||
response.content = response.content
|
||||
.replace(/{{COMMIT}}/g, this.options.commit)
|
||||
.replace(/{{BASE}}/g, this.base(route))
|
||||
.replace(/{{APP_NAME}}/g, name)
|
||||
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify(options)}'`)
|
||||
.replace(/{{APP_NAME}}/, name)
|
||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
||||
return response
|
||||
}
|
||||
|
||||
@ -108,8 +114,8 @@ export class MainHttpProvider extends HttpProvider {
|
||||
}
|
||||
|
||||
private getAppRow(app: Application): string {
|
||||
return `<div class="app-row">
|
||||
<a class="open" href=".${app.path}">
|
||||
return `<div class="block-row">
|
||||
<a class="item -row -link" href=".${app.path}">
|
||||
${
|
||||
app.icon
|
||||
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
|
||||
@ -127,4 +133,46 @@ export class MainHttpProvider extends HttpProvider {
|
||||
}
|
||||
</div>`
|
||||
}
|
||||
|
||||
private async getUpdate(): Promise<string> {
|
||||
if (!this.update.enabled) {
|
||||
return "Updates are disabled"
|
||||
}
|
||||
|
||||
const humanize = (time: number): string => {
|
||||
const d = new Date(time)
|
||||
const pad = (t: number): string => (t < 10 ? "0" : "") + t
|
||||
return (
|
||||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
||||
` ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
)
|
||||
}
|
||||
|
||||
const update = await this.update.getUpdate()
|
||||
if (this.update.isLatestVersion(update)) {
|
||||
return `<div class="block-row">
|
||||
<div class="item">
|
||||
${update.version}
|
||||
<div class="sub">Up to date</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
${humanize(update.checked)}
|
||||
<a class="sub -link" href="./update/check">Check now</a>
|
||||
</div>
|
||||
<div class="item" >Current: ${this.update.currentVersion}</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
return `<div class="block-row">
|
||||
<a class="item -link" href="./update">
|
||||
${update.version}
|
||||
<div class="sub">Out of date</div>
|
||||
</a>
|
||||
<div class="item">
|
||||
${humanize(update.checked)}
|
||||
<a class="sub -link" href="./update/check">Check now</a>
|
||||
</div>
|
||||
<div class="item" >Current: ${this.update.currentVersion}</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
@ -50,8 +50,8 @@ export class LoginHttpProvider extends HttpProvider {
|
||||
response.content = response.content
|
||||
.replace(/{{COMMIT}}/g, this.options.commit)
|
||||
.replace(/{{BASE}}/g, this.base(route))
|
||||
.replace(/{{VALUE}}/g, value || "")
|
||||
.replace(/{{ERROR}}/g, error ? `<div class="error">${error.message}</div>` : "")
|
||||
.replace(/{{VALUE}}/, value || "")
|
||||
.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
||||
return response
|
||||
}
|
||||
|
||||
|
322
src/node/app/update.ts
Normal file
322
src/node/app/update.ts
Normal file
@ -0,0 +1,322 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import zip from "adm-zip"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import * as https from "https"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as semver from "semver"
|
||||
import { Readable, Writable } from "stream"
|
||||
import * as tar from "tar-fs"
|
||||
import * as url from "url"
|
||||
import * as util from "util"
|
||||
import * as zlib from "zlib"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { settings } from "../settings"
|
||||
import { tmpdir } from "../util"
|
||||
import { ipcMain } from "../wrapper"
|
||||
|
||||
export interface Update {
|
||||
checked: number
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Update HTTP provider.
|
||||
*/
|
||||
export class UpdateHttpProvider extends HttpProvider {
|
||||
private update?: Promise<Update>
|
||||
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
|
||||
|
||||
public constructor(options: HttpProviderOptions, public readonly enabled: boolean) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
|
||||
switch (route.base) {
|
||||
case "/check":
|
||||
this.ensureMethod(request)
|
||||
this.getUpdate(true)
|
||||
return { redirect: "/login" }
|
||||
case "/": {
|
||||
this.ensureMethod(request, ["GET", "POST"])
|
||||
if (route.requestPath !== "/index.html") {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
} else if (!this.authenticated(request)) {
|
||||
return { redirect: "/login" }
|
||||
}
|
||||
|
||||
switch (request.method) {
|
||||
case "GET":
|
||||
return this.getRoot(route)
|
||||
case "POST":
|
||||
return this.tryUpdate(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html")
|
||||
response.content = response.content
|
||||
.replace(/{{COMMIT}}/g, this.options.commit)
|
||||
.replace(/{{BASE}}/g, this.base(route))
|
||||
.replace(/{{UPDATE_STATUS}}/, await this.getUpdateHtml())
|
||||
.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
||||
return response
|
||||
}
|
||||
|
||||
public async handleWebSocket(): Promise<undefined> {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for and return the latest update.
|
||||
*/
|
||||
public async getUpdate(force?: boolean): Promise<Update> {
|
||||
if (!this.enabled) {
|
||||
throw new Error("updates are not enabled")
|
||||
}
|
||||
|
||||
if (!this.update) {
|
||||
this.update = this._getUpdate(force)
|
||||
this.update.then(() => (this.update = undefined))
|
||||
}
|
||||
|
||||
return this.update
|
||||
}
|
||||
|
||||
private async _getUpdate(force?: boolean): Promise<Update> {
|
||||
const url = "https://api.github.com/repos/cdr/code-server/releases/latest"
|
||||
const now = Date.now()
|
||||
try {
|
||||
let { update } = !force ? await settings.read() : { update: undefined }
|
||||
if (!update || update.checked + this.updateInterval < now) {
|
||||
const buffer = await this.request(url)
|
||||
const data = JSON.parse(buffer.toString())
|
||||
update = { checked: now, version: data.name as string }
|
||||
settings.write({ update })
|
||||
}
|
||||
logger.debug("Got latest version", field("latest", update.version))
|
||||
return update
|
||||
} catch (error) {
|
||||
logger.error("Failed to get latest version", field("error", error.message))
|
||||
return {
|
||||
checked: now,
|
||||
version: "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get currentVersion(): string {
|
||||
return require(path.resolve(__dirname, "../../../package.json")).version
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the currently installed version is the latest.
|
||||
*/
|
||||
public isLatestVersion(latest: Update): boolean {
|
||||
const version = this.currentVersion
|
||||
logger.debug("Comparing versions", field("current", version), field("latest", latest.version))
|
||||
try {
|
||||
return latest.version === version || semver.lt(latest.version, version)
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private async getUpdateHtml(): Promise<string> {
|
||||
if (!this.enabled) {
|
||||
return "Updates are disabled"
|
||||
}
|
||||
|
||||
const update = await this.getUpdate()
|
||||
if (this.isLatestVersion(update)) {
|
||||
throw new Error("No update available")
|
||||
}
|
||||
|
||||
return `<button type="submit" class="apply">
|
||||
Update to ${update.version}
|
||||
</button>
|
||||
<div class="current">Current: ${this.currentVersion}</div>`
|
||||
}
|
||||
|
||||
public async tryUpdate(route: Route): Promise<HttpResponse> {
|
||||
try {
|
||||
const update = await this.getUpdate()
|
||||
if (this.isLatestVersion(update)) {
|
||||
throw new Error("no update available")
|
||||
}
|
||||
await this.downloadUpdate(update)
|
||||
return {
|
||||
redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/",
|
||||
}
|
||||
} catch (error) {
|
||||
return this.getRoot(route, error)
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadUpdate(update: Update): Promise<void> {
|
||||
const releaseName = await this.getReleaseName(update)
|
||||
const url = `https://github.com/cdr/code-server/releases/download/${update.version.replace}/${releaseName}`
|
||||
|
||||
await fs.mkdirp(tmpdir)
|
||||
|
||||
const response = await this.requestResponse(url)
|
||||
|
||||
try {
|
||||
let downloadPath = path.join(tmpdir, releaseName)
|
||||
if (downloadPath.endsWith(".tar.gz")) {
|
||||
downloadPath = await this.extractTar(response, downloadPath)
|
||||
} else {
|
||||
downloadPath = await this.extractZip(response, downloadPath)
|
||||
}
|
||||
logger.debug("Downloaded update", field("path", downloadPath))
|
||||
|
||||
const target = path.resolve(__dirname, "../")
|
||||
logger.debug("Replacing files", field("target", target))
|
||||
await fs.unlink(target)
|
||||
await fs.move(downloadPath, target)
|
||||
|
||||
ipcMain().relaunch(update.version)
|
||||
} catch (error) {
|
||||
response.destroy(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async extractTar(response: Readable, downloadPath: string): Promise<string> {
|
||||
downloadPath = downloadPath.replace(/\.tar\.gz$/, "")
|
||||
logger.debug("Extracting tar", field("path", downloadPath))
|
||||
|
||||
response.pause()
|
||||
await fs.remove(downloadPath)
|
||||
|
||||
const decompress = zlib.createGunzip()
|
||||
response.pipe(decompress as Writable)
|
||||
response.on("error", (error) => decompress.destroy(error))
|
||||
response.on("close", () => decompress.end())
|
||||
|
||||
const destination = tar.extract(downloadPath)
|
||||
decompress.pipe(destination)
|
||||
decompress.on("error", (error) => destination.destroy(error))
|
||||
decompress.on("close", () => destination.end())
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
destination.on("finish", resolve)
|
||||
destination.on("error", reject)
|
||||
response.resume()
|
||||
})
|
||||
|
||||
return downloadPath
|
||||
}
|
||||
|
||||
private async extractZip(response: Readable, downloadPath: string): Promise<string> {
|
||||
logger.debug("Downloading zip", field("path", downloadPath))
|
||||
|
||||
response.pause()
|
||||
await fs.remove(downloadPath)
|
||||
|
||||
const write = fs.createWriteStream(downloadPath)
|
||||
response.pipe(write)
|
||||
response.on("error", (error) => write.destroy(error))
|
||||
response.on("close", () => write.end())
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
write.on("error", reject)
|
||||
write.on("close", resolve)
|
||||
response.resume
|
||||
})
|
||||
|
||||
const zipPath = downloadPath
|
||||
downloadPath = downloadPath.replace(/\.zip$/, "")
|
||||
await fs.remove(downloadPath)
|
||||
|
||||
logger.debug("Extracting zip", field("path", zipPath))
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
new zip(zipPath).extractAllToAsync(downloadPath, true, (error) => {
|
||||
return error ? reject(error) : resolve()
|
||||
})
|
||||
})
|
||||
|
||||
await fs.remove(zipPath)
|
||||
|
||||
return downloadPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an update return the name for the packaged archived.
|
||||
*/
|
||||
private async getReleaseName(update: Update): Promise<string> {
|
||||
let target: string = os.platform()
|
||||
if (target === "linux") {
|
||||
const result = await util
|
||||
.promisify(cp.exec)("ldd --version")
|
||||
.catch((error) => ({
|
||||
stderr: error.message,
|
||||
stdout: "",
|
||||
}))
|
||||
if (/musl/.test(result.stderr) || /musl/.test(result.stdout)) {
|
||||
target = "alpine"
|
||||
}
|
||||
}
|
||||
let arch = os.arch()
|
||||
if (arch === "x64") {
|
||||
arch = "x86_64"
|
||||
}
|
||||
return `code-server-${update.version}-${target}-${arch}.${target === "darwin" ? "zip" : "tar.gz"}`
|
||||
}
|
||||
|
||||
private async request(uri: string): Promise<Buffer> {
|
||||
const response = await this.requestResponse(uri)
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = []
|
||||
let bufferLength = 0
|
||||
response.on("data", (chunk) => {
|
||||
bufferLength += chunk.length
|
||||
chunks.push(chunk)
|
||||
})
|
||||
response.on("error", reject)
|
||||
response.on("end", () => {
|
||||
resolve(Buffer.concat(chunks, bufferLength))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async requestResponse(uri: string): Promise<http.IncomingMessage> {
|
||||
let redirects = 0
|
||||
const maxRedirects = 10
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = (uri: string): void => {
|
||||
logger.debug("Making request", field("uri", uri))
|
||||
https.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
|
||||
if (
|
||||
response.statusCode &&
|
||||
response.statusCode >= 300 &&
|
||||
response.statusCode < 400 &&
|
||||
response.headers.location
|
||||
) {
|
||||
++redirects
|
||||
if (redirects > maxRedirects) {
|
||||
return reject(new Error("reached max redirects"))
|
||||
}
|
||||
response.destroy()
|
||||
return request(url.resolve(uri, response.headers.location))
|
||||
}
|
||||
|
||||
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
||||
return reject(new Error(`${response.statusCode || "500"}`))
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
}
|
||||
request(uri)
|
||||
})
|
||||
}
|
||||
}
|
@ -17,17 +17,11 @@ import { HttpCode, HttpError } from "../../common/http"
|
||||
import { generateUuid } from "../../common/util"
|
||||
import { Args } from "../cli"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { SettingsProvider } from "../settings"
|
||||
import { xdgLocalDir } from "../util"
|
||||
|
||||
export interface Settings {
|
||||
lastVisited: StartPath
|
||||
}
|
||||
import { settings } from "../settings"
|
||||
|
||||
export class VscodeHttpProvider extends HttpProvider {
|
||||
private readonly serverRootPath: string
|
||||
private readonly vsRootPath: string
|
||||
private readonly settings = new SettingsProvider<Settings>(path.join(xdgLocalDir, "coder.json"))
|
||||
private _vscode?: Promise<cp.ChildProcess>
|
||||
private workbenchOptions?: WorkbenchOptions
|
||||
|
||||
@ -178,12 +172,12 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
|
||||
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
const remoteAuthority = request.headers.host as string
|
||||
const settings = await this.settings.read()
|
||||
const { lastVisited } = await settings.read()
|
||||
const startPath = await this.getFirstValidPath(
|
||||
[
|
||||
{ url: route.query.workspace, workspace: true },
|
||||
{ url: route.query.folder, workspace: false },
|
||||
settings.lastVisited,
|
||||
lastVisited,
|
||||
this.args._ && this.args._.length > 0 ? { url: this.args._[0] } : undefined,
|
||||
],
|
||||
remoteAuthority,
|
||||
@ -200,7 +194,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
this.workbenchOptions = options
|
||||
|
||||
if (startPath) {
|
||||
this.settings.write({
|
||||
settings.write({
|
||||
lastVisited: startPath,
|
||||
})
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ export interface Args extends VsArgs {
|
||||
readonly auth?: AuthType
|
||||
readonly cert?: OptionalString
|
||||
readonly "cert-key"?: string
|
||||
readonly "disable-updates"?: boolean
|
||||
readonly help?: boolean
|
||||
readonly host?: string
|
||||
readonly json?: boolean
|
||||
@ -66,6 +67,7 @@ const options: Options<Required<Args>> = {
|
||||
description: "Path to certificate. Generated if no path is provided.",
|
||||
},
|
||||
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
||||
"disable-updates": { type: "boolean", description: "Disable automatic updates." },
|
||||
host: { type: "string", description: "Host for the HTTP server." },
|
||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||
json: { type: "boolean" },
|
||||
|
@ -3,6 +3,7 @@ import { Args, optionDescriptions, parse } from "./cli"
|
||||
import { ApiHttpProvider } from "./app/api"
|
||||
import { MainHttpProvider } from "./app/app"
|
||||
import { LoginHttpProvider } from "./app/login"
|
||||
import { UpdateHttpProvider } from "./app/update"
|
||||
import { VscodeHttpProvider } from "./app/vscode"
|
||||
import { AuthType, HttpServer } from "./http"
|
||||
import { generateCertificate, generatePassword, hash, open } from "./util"
|
||||
@ -41,9 +42,10 @@ const main = async (args: Args): Promise<void> => {
|
||||
|
||||
const httpServer = new HttpServer(options)
|
||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer)
|
||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
||||
httpServer.registerHttpProvider("/vscode", VscodeHttpProvider, args)
|
||||
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
||||
httpServer.registerHttpProvider("/", MainHttpProvider, api)
|
||||
httpServer.registerHttpProvider("/", MainHttpProvider, api, update)
|
||||
|
||||
ipcMain().onDispose(() => httpServer.dispose())
|
||||
|
||||
@ -72,6 +74,8 @@ const main = async (args: Args): Promise<void> => {
|
||||
logger.info(" - Not serving HTTPS")
|
||||
}
|
||||
|
||||
logger.info(` - Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||
|
||||
if (serverAddress && !options.socket && args.open) {
|
||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
||||
|
@ -355,6 +355,10 @@ export interface HttpProvider1<A1, T> {
|
||||
new (options: HttpProviderOptions, a1: A1): T
|
||||
}
|
||||
|
||||
export interface HttpProvider2<A1, A2, T> {
|
||||
new (options: HttpProviderOptions, a1: A1, a2: A2): T
|
||||
}
|
||||
|
||||
/**
|
||||
* An HTTP server. Its main role is to route incoming HTTP requests to the
|
||||
* appropriate provider for that endpoint then write out the response. It also
|
||||
@ -404,8 +408,14 @@ export class HttpServer {
|
||||
*/
|
||||
public registerHttpProvider<T extends HttpProvider>(endpoint: string, provider: HttpProvider0<T>): T
|
||||
public registerHttpProvider<A1, T extends HttpProvider>(endpoint: string, provider: HttpProvider1<A1, T>, a1: A1): T
|
||||
public registerHttpProvider<A1, A2, T extends HttpProvider>(
|
||||
endpoint: string,
|
||||
provider: HttpProvider2<A1, A2, T>,
|
||||
a1: A1,
|
||||
a2: A2,
|
||||
): T
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public registerHttpProvider(endpoint: string, provider: any, a1?: any): any {
|
||||
public registerHttpProvider(endpoint: string, provider: any, ...args: any[]): any {
|
||||
endpoint = endpoint.replace(/^\/+|\/+$/g, "")
|
||||
if (this.providers.has(`/${endpoint}`)) {
|
||||
throw new Error(`${endpoint} is already registered`)
|
||||
@ -420,7 +430,7 @@ export class HttpServer {
|
||||
commit: this.options.commit,
|
||||
password: this.options.password,
|
||||
},
|
||||
a1,
|
||||
...args,
|
||||
)
|
||||
this.providers.set(`/${endpoint}`, p)
|
||||
return p
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as fs from "fs-extra"
|
||||
import * as path from "path"
|
||||
import { extend, xdgLocalDir } from "./util"
|
||||
import { logger } from "@coder/logger"
|
||||
import { extend } from "./util"
|
||||
|
||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||
|
||||
@ -32,9 +33,28 @@ export class SettingsProvider<T> {
|
||||
*/
|
||||
public async write(settings: Partial<T>): Promise<void> {
|
||||
try {
|
||||
await fs.writeFile(this.settingsPath, JSON.stringify(extend(this.read(), settings)))
|
||||
await fs.writeFile(this.settingsPath, JSON.stringify(extend(await this.read(), settings), null, 2))
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global code-server settings.
|
||||
*/
|
||||
export interface CoderSettings {
|
||||
lastVisited: {
|
||||
url: string
|
||||
workspace: boolean
|
||||
}
|
||||
update: {
|
||||
checked: number
|
||||
version: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global code-server settings file.
|
||||
*/
|
||||
export const settings = new SettingsProvider<CoderSettings>(path.join(xdgLocalDir, "coder.json"))
|
||||
|
Reference in New Issue
Block a user