mirror of
https://github.com/purocean/yn.git
synced 2026-09-24 15:46:02 +08:00
feat: handle readonly file
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '默认预览器',
|
||||
|
||||
@@ -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': '預設預覽器',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user