feat: handle readonly file

This commit is contained in:
purocean
2024-07-30 12:22:05 +08:00
parent 7ba2e33165
commit 8175937594
12 changed files with 104 additions and 12 deletions
+15
View File
@@ -164,6 +164,21 @@ export function stat (repo: string, p: string) {
}, p)
}
export function checkWriteable (repo: string, p: string) {
return withRepo(repo, async (_, targetPath) => {
if (readonly) {
return false
}
try {
await fs.access(targetPath, fs.constants.W_OK)
return true
} catch (error) {
return false
}
}, p)
}
export function write (repo: string, p: string, content: any): Promise<string> {
if (readonly) throw new Error('Readonly')
+7 -3
View File
@@ -22,6 +22,7 @@ import config from '../config'
import * as jwt from '../jwt'
import { getAction } from '../action'
import * as extension from '../extension'
import type { FileReadResult } from '../../share/types'
const isLocalhost = (address: string) => {
return ip.isEqual(address, '127.0.0.1') || ip.isEqual(address, '::1')
@@ -122,11 +123,14 @@ const fileContent = async (ctx: any, next: any) => {
const content = await file.read(repo, path)
ctx.body = result('ok', 'success', {
const data: FileReadResult = {
content: content.toString(asBase64 ? 'base64' : undefined),
hash: await file.hash(repo, path),
stat: await file.stat(repo, path)
})
stat: await file.stat(repo, path),
writeable: await file.checkWriteable(repo, path),
}
ctx.body = result('ok', 'success', data)
} else if (ctx.method === 'POST') {
const { oldHash, content, asBase64, repo, path } = ctx.request.body
+1 -1
View File
@@ -110,7 +110,7 @@ export default defineComponent({
await nextTick()
getEditor().updateOptions({
readOnly: FLAG_READONLY || !current || !current.plain
readOnly: FLAG_READONLY || !current || !current.plain || current.writeable === false
})
if (isDefault()) {
+3 -1
View File
@@ -338,11 +338,13 @@ export default defineComponent({
const fileTabs = computed(() => (tabs.value as Components.FileTabs.Item[]).map(tab => {
if (currentFile.value && tab.key === toUri(currentFile.value)) {
const status = currentFile.value.status
const { status, writeable } = currentFile.value
let mark = ''
if (!isSaved.value) {
mark = '*'
} else if (writeable === false) {
mark = '🔒'
} else if (status === 'saved') {
mark = ''
} else if (status === 'save-failed') {
+41
View File
@@ -3,6 +3,7 @@ import dayjs from 'dayjs'
import { getEditor, getOneIndent, insert, whenEditorReady } from '@fe/services/editor'
import type { Plugin } from '@fe/context'
import { t } from '@fe/services/i18n'
import type { IMarkdownString } from 'monaco-editor'
export default {
name: 'editor-markdown',
@@ -37,6 +38,8 @@ export default {
const idRevealLineInPreview = 'plugin.editor.reveal-line-in-preview'
const idForceInsertNewLine = 'plugin.editor.force-insert-new-line'
const idForceInsertIndent = 'plugin.editor.force-insert-indent'
const idRevealCurrentFileInOS = 'plugin.editor.reveal-current-file-in-os'
const idRefreshCurrentDoc = 'plugin.editor.refresh-current-document'
whenEditorReady().then(({ editor, monaco }) => {
const KM = monaco.KeyMod
@@ -110,6 +113,44 @@ export default {
editor.onDidCompositionEnd(() => {
ctx.store.state.inComposition = false
})
const messageContribution: any = editor.getContribution('editor.contrib.messageController')
editor.onDidAttemptReadOnlyEdit(() => {
const currentFile = ctx.store.state.currentFile
const cmdRevealCurrentFile = `command:vs.editor.ICodeEditor:1:${idRevealCurrentFileInOS}`
const cmdRefreshCurrentDoc = `command:vs.editor.ICodeEditor:1:${idRefreshCurrentDoc}`
const message =
ctx.args.FLAG_READONLY
? ctx.i18n.t('read-only-mode-desc')
: !currentFile
? ctx.i18n.t('file-status.no-file')
: currentFile.writeable === false
? {
value: ctx.i18n.t('file-readonly-desc', cmdRevealCurrentFile, cmdRefreshCurrentDoc),
isTrusted: true,
} as IMarkdownString
: ctx.i18n.t('can-not-edit-this-file-type')
messageContribution.showMessage(message, editor.getPosition())
})
editor.addAction({
id: idRevealCurrentFileInOS,
label: ctx.i18n.t('editor.action-label.reveal-current-file-in-os'),
run () {
const filePath = ctx.store.state.currentFile?.absolutePath
if (filePath) {
ctx.base.showItemInFolder(filePath)
}
}
})
editor.addAction({
id: idRefreshCurrentDoc,
label: ctx.i18n.t('editor.action-label.refresh-current-document'),
run () {
ctx.view.refresh()
}
})
})
ctx.statusBar.tapMenus(menus => {
+5 -2
View File
@@ -10,7 +10,7 @@ import * as crypto from '@fe/utils/crypto'
import { useModal } from '@fe/support/ui/modal'
import { useToast } from '@fe/support/ui/toast'
import store from '@fe/support/store'
import { DocType, type Doc, type DocCategory, type PathItem, type SwitchDocOpts } from '@fe/types'
import type { DocType, FileStat, Doc, DocCategory, PathItem, SwitchDocOpts } from '@fe/types'
import { basename, dirname, extname, isBelongTo, join, normalizeSep, relative, resolve } from '@fe/utils/path'
import { getActionHandler } from '@fe/core/action'
import { triggerHook } from '@fe/core/hook'
@@ -717,7 +717,8 @@ async function _switchDoc (doc: Doc | null, opts?: SwitchDocOpts): Promise<void>
let content = ''
let hash = ''
let stat
let stat: FileStat | undefined
let writeable: boolean | undefined
if (doc.plain) {
const timer = setTimeout(() => {
store.state.currentFile = { ...doc!, status: undefined }
@@ -730,6 +731,7 @@ async function _switchDoc (doc: Doc | null, opts?: SwitchDocOpts): Promise<void>
content = res.content
hash = res.hash
stat = res.stat
writeable = res.writeable
}
// decrypt content.
@@ -744,6 +746,7 @@ async function _switchDoc (doc: Doc | null, opts?: SwitchDocOpts): Promise<void>
store.state.currentFile = {
...doc,
stat,
writeable,
content,
passwordHash,
contentHash: hash,
-2
View File
@@ -1,6 +1,5 @@
import type * as Monaco from 'monaco-editor'
import { cloneDeep, debounce } from 'lodash-es'
import { FLAG_READONLY } from '@fe/support/args'
import { isElectron, isMacOS } from '@fe/support/env'
import { registerHook, triggerHook } from '@fe/core/hook'
import { getActionHandler, registerAction } from '@fe/core/action'
@@ -79,7 +78,6 @@ export const getDefaultOptions = (): Monaco.editor.IStandaloneEditorConstruction
vertical: 'hidden',
verticalScrollbarSize: 0
} : undefined,
readOnly: FLAG_READONLY,
acceptSuggestionOnEnter: 'smart',
unicodeHighlight: {
ambiguousCharacters: false,
+3 -3
View File
@@ -1,7 +1,7 @@
import type { Stats } from 'fs'
import type { WatchOptions } from 'chokidar'
import type { IProgressMessage, ISerializedFileMatch, ISerializedSearchSuccess, ITextQuery } from 'ripgrep-wrapper'
import type { Components, Doc, ExportType, FileItem, FileSort, FileStat, PathItem } from '@fe/types'
import type { Components, Doc, ExportType, FileItem, FileReadResult, FileSort, FileStat, PathItem } from '@fe/types'
import { isElectron } from '@fe/support/env'
import { HELP_REPO_NAME, JWT_TOKEN } from './args'
@@ -92,7 +92,7 @@ export async function proxyFetch (url: string, init?: Omit<RequestInit, 'body'>
*/
async function fetchHelpContent (docName: string) {
const result = await fetchHttp('/api/help?doc=' + docName)
return { content: result.data.content, hash: '', stat: { mtime: 0, birthtime: 0, size: 0 } }
return { content: result.data.content, hash: '', stat: { mtime: 0, birthtime: 0, size: 0 }, writeable: true }
}
/**
@@ -101,7 +101,7 @@ async function fetchHelpContent (docName: string) {
* @param asBase64
* @returns
*/
export async function readFile (file: PathItem, asBase64 = false): Promise<{content: string, hash: string, stat: FileStat}> {
export async function readFile (file: PathItem, asBase64 = false): Promise<FileReadResult> {
const { path, repo } = file
if (repo === HELP_REPO_NAME) {
+7
View File
@@ -41,6 +41,9 @@ const data = {
'switch-repo-n': 'Switch to the repository [%s]',
'switch-the-last-repo': 'Switch to the last repository',
'edit-or-generate-text-using-ai': 'Edit or generate text using AI',
'file-readonly-desc': 'The file is read-only. You can [change to writable](%s) in the system and then [refresh](%s) the document to edit.',
'can-not-edit-this-file-type': 'Cannot edit this file type',
'read-only-mode-desc': 'The application is currently in read-only mode and cannot be edited.',
'premium': {
'confetti': 'Confetti',
'need-purchase': '[%s] Premium is required',
@@ -484,6 +487,10 @@ const data = {
},
'switch-editor': 'Switch Editor',
'default-editor': 'Default Editor',
'action-label': {
'reveal-current-file-in-os': 'Reveal Current File in OS',
'refresh-current-document': 'Refresh Current Document',
},
},
'previewer': {
'default-previewer': 'Default Previewer',
+7
View File
@@ -42,6 +42,9 @@ const data: BaseLanguage = {
'switch-repo-n': '切换到仓库 %s',
'switch-the-last-repo': '切换到最后一个仓库',
'edit-or-generate-text-using-ai': '使用 AI Copilot 编辑或生成文字',
'file-readonly-desc': '当前文件只读,如需编辑请先在系统中[改为可写](%s),然后[刷新](%s)当前文档。',
'can-not-edit-this-file-type': '不能编辑此类型文件',
'read-only-mode-desc': '当前是只读模式,不可编辑',
'premium': {
'confetti': '彩色纸屑',
'need-purchase': '[%s] 需要高级版',
@@ -485,6 +488,10 @@ const data: BaseLanguage = {
},
'switch-editor': '切换编辑器',
'default-editor': '默认编辑器',
'action-label': {
'reveal-current-file-in-os': '在系统中显示当前文件',
'refresh-current-document': '刷新当前文档',
},
},
'previewer': {
'default-previewer': '默认预览器',
+7
View File
@@ -42,6 +42,9 @@ const data: BaseLanguage = {
'switch-repo-n': '切換到仓库 %s',
'switch-the-last-repo': '切換到最後一個仓库',
'edit-or-generate-text-using-ai': '使用 AI Copilot 編輯或生成文本',
'file-readonly-desc': '當前文件是唯讀的。如需編輯,請先在系統中[更改為可寫](%s),然後[刷新](%s)當前文檔。',
'can-not-edit-this-file-type': '不能編輯此類型的文件',
'read-only-mode-desc': '當前應用處於唯讀模式,不可編輯',
'premium': {
'confetti': '彩色紙屑',
'need-purchase': '[%s] 需高級版',
@@ -485,6 +488,10 @@ const data: BaseLanguage = {
},
'switch-editor': '切換編輯器',
'default-editor': '默認編輯器',
'action-label': {
'reveal-current-file-in-os': '在系統中顯示當前文件',
'refresh-current-document': '刷新當前文件',
}
},
'previewer': {
'default-previewer': '預設預覽器',
+8
View File
@@ -11,6 +11,13 @@ export interface FileStat {
size: number,
}
export interface FileReadResult {
content: string,
stat: FileStat,
hash: string,
writeable: boolean,
}
export interface Doc extends PathItem {
type: 'file' | 'dir';
name: string;
@@ -19,6 +26,7 @@ export interface Doc extends PathItem {
passwordHash?: string;
contentHash?: string;
stat?: FileStat,
writeable?: boolean,
status?: 'loaded' | 'save-failed' | 'saved' | 'unsaved';
absolutePath?: string,
plain?: boolean;