Skip to content

Commit

Permalink
Rework userFileStore (#815)
Browse files Browse the repository at this point in the history
* Rework userFileStore

* nit

* Add back unittests
  • Loading branch information
huchenlei authored Sep 13, 2024
1 parent 65a8dbb commit 17db1e6
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 277 deletions.
16 changes: 15 additions & 1 deletion src/scripts/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
PromptResponse,
SystemStats,
User,
Settings
Settings,
UserDataFullInfo
} from '@/types/apiTypes'
import axios from 'axios'

Expand Down Expand Up @@ -671,6 +672,19 @@ class ComfyApi extends EventTarget {
return resp.json()
}

async listUserDataFullInfo(dir: string): Promise<UserDataFullInfo[]> {
const resp = await this.fetchApi(
`/userdata?dir=${encodeURIComponent(dir)}&recurse=true&split=false&full_info=true`
)
if (resp.status === 404) return []
if (resp.status !== 200) {
throw new Error(
`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`
)
}
return resp.json()
}

async getLogs(): Promise<string> {
return (await axios.get(this.internalURL('/logs'))).data
}
Expand Down
273 changes: 119 additions & 154 deletions src/stores/userFileStore.ts
Original file line number Diff line number Diff line change
@@ -1,173 +1,138 @@
import { defineStore } from 'pinia'
import { api } from '@/scripts/api'
import type { TreeNode } from 'primevue/treenode'
import { buildTree } from '@/utils/treeUtil'
import { computed, ref } from 'vue'
import { TreeExplorerNode } from '@/types/treeExplorerTypes'

export class UserFile {
isLoading: boolean = false
content: string | null = null
originalContent: string | null = null

constructor(
public path: string,
public lastModified: number,
public size: number
) {}

get isOpen() {
return !!this.content
}

interface OpenFile {
path: string
content: string
isModified: boolean
originalContent: string
}

interface StoreState {
files: string[]
openFiles: OpenFile[]
}

interface ApiResponse<T = any> {
success: boolean
data?: T
message?: string
get isModified() {
return this.content !== this.originalContent
}
}

export const useUserFileStore = defineStore('userFile', {
state: (): StoreState => ({
files: [],
openFiles: []
}),

getters: {
getOpenFile: (state) => (path: string) =>
state.openFiles.find((file) => file.path === path),
modifiedFiles: (state) => state.openFiles.filter((file) => file.isModified),
workflowsTree: (state): TreeNode =>
buildTree(state.files, (path: string) => path.split('/'))
},

actions: {
async openFile(path: string): Promise<void> {
if (this.getOpenFile(path)) return

const { success, data } = await this.getFileData(path)
if (success && data) {
this.openFiles.push({
path,
content: data,
isModified: false,
originalContent: data
})
}
},

closeFile(path: string): void {
const index = this.openFiles.findIndex((file) => file.path === path)
if (index !== -1) {
this.openFiles.splice(index, 1)
}
},

updateFileContent(path: string, newContent: string): void {
const file = this.getOpenFile(path)
if (file) {
file.content = newContent
file.isModified = file.content !== file.originalContent
}
},

async saveOpenFile(path: string): Promise<ApiResponse> {
const file = this.getOpenFile(path)
if (file?.isModified) {
const result = await this.saveFile(path, file.content)
if (result.success) {
file.isModified = false
file.originalContent = file.content
}
return result
}
return { success: true }
},

discardChanges(path: string): void {
const file = this.getOpenFile(path)
if (file) {
file.content = file.originalContent
file.isModified = false
}
},

async loadFiles(dir: string = './'): Promise<void> {
this.files = (await api.listUserData(dir, true, false)).map(
(filePath: string) => filePath.replaceAll('\\', '/')
)

this.openFiles = (
await Promise.all(
this.openFiles.map(async (openFile) => {
if (!this.files.includes(openFile.path)) return null

const { success, data } = await this.getFileData(openFile.path)
if (success && data !== openFile.originalContent) {
return {
...openFile,
content: data,
originalContent: data,
isModified: openFile.content !== data
}
}

return openFile
})
export const useUserFileStore = defineStore('userFile', () => {
const userFilesByPath = ref(new Map<string, UserFile>())

const userFiles = computed(() => Array.from(userFilesByPath.value.values()))
const modifiedFiles = computed(() =>
userFiles.value.filter((file: UserFile) => file.isModified)
)
const openedFiles = computed(() =>
userFiles.value.filter((file: UserFile) => file.isOpen)
)

const workflowsTree = computed<TreeExplorerNode<UserFile>>(
() =>
buildTree<UserFile>(userFiles.value, (userFile: UserFile) =>
userFile.path.split('/')
) as TreeExplorerNode<UserFile>
)

/**
* Syncs the files in the given directory with the API.
* @param dir The directory to sync.
*/
const syncFiles = async () => {
const files = await api.listUserDataFullInfo('')

for (const file of files) {
const existingFile = userFilesByPath.value.get(file.path)

if (!existingFile) {
// New file, add it to the map
userFilesByPath.value.set(
file.path,
new UserFile(file.path, file.modified, file.size)
)
).filter((file): file is OpenFile => file !== null)
},

async renameFile(oldPath: string, newPath: string): Promise<ApiResponse> {
const resp = await api.moveUserData(oldPath, newPath)
if (resp.status !== 200) {
return { success: false, message: resp.statusText }
}

const openFile = this.openFiles.find((file) => file.path === oldPath)
if (openFile) {
openFile.path = newPath
}

await this.loadFiles()
return { success: true }
},

async deleteFile(path: string): Promise<ApiResponse> {
const resp = await api.deleteUserData(path)
if (resp.status !== 204) {
return {
success: false,
message: `Error removing user data file '${path}': ${resp.status} ${resp.statusText}`
}
} else if (existingFile.lastModified !== file.modified) {
// File has been modified, update its properties
existingFile.lastModified = file.modified
existingFile.size = file.size
existingFile.originalContent = null
existingFile.content = null
existingFile.isLoading = false
}
}

const index = this.openFiles.findIndex((file) => file.path === path)
if (index !== -1) {
this.openFiles.splice(index, 1)
// Remove files that no longer exist
for (const [path, _] of userFilesByPath.value) {
if (!files.some((file) => file.path === path)) {
userFilesByPath.value.delete(path)
}
}
}

await this.loadFiles()
return { success: true }
},
const loadFile = async (file: UserFile) => {
file.isLoading = true
const resp = await api.getUserData(file.path)
if (resp.status !== 200) {
throw new Error(
`Failed to load file '${file.path}': ${resp.status} ${resp.statusText}`
)
}
file.content = await resp.text()
file.originalContent = file.content
file.isLoading = false
}

async saveFile(path: string, data: string): Promise<ApiResponse> {
const resp = await api.storeUserData(path, data, {
stringify: false,
throwOnError: false,
overwrite: true
})
const saveFile = async (file: UserFile) => {
if (file.isModified) {
const resp = await api.storeUserData(file.path, file.content)
if (resp.status !== 200) {
return {
success: false,
message: `Error saving user data file '${path}': ${resp.status} ${resp.statusText}`
}
throw new Error(
`Failed to save file '${file.path}': ${resp.status} ${resp.statusText}`
)
}
}
await syncFiles()
}

await this.loadFiles()
return { success: true }
},
const deleteFile = async (file: UserFile) => {
const resp = await api.deleteUserData(file.path)
if (resp.status !== 204) {
throw new Error(
`Failed to delete file '${file.path}': ${resp.status} ${resp.statusText}`
)
}
await syncFiles()
}

async getFileData(path: string): Promise<ApiResponse<string>> {
const resp = await api.getUserData(path)
if (resp.status !== 200) {
return { success: false, message: resp.statusText }
}
return { success: true, data: await resp.json() }
const renameFile = async (file: UserFile, newPath: string) => {
const resp = await api.moveUserData(file.path, newPath)
if (resp.status !== 200) {
throw new Error(
`Failed to rename file '${file.path}': ${resp.status} ${resp.statusText}`
)
}
file.path = newPath
userFilesByPath.value.set(newPath, file)
userFilesByPath.value.delete(file.path)
await syncFiles()
}

return {
userFiles,
modifiedFiles,
openedFiles,
workflowsTree,
syncFiles,
loadFile,
saveFile,
deleteFile,
renameFile
}
})
7 changes: 6 additions & 1 deletion src/types/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,11 @@ const zUser = z.object({
users: z.record(z.string(), z.unknown())
})
const zUserData = z.array(z.array(z.string(), z.string()))

const zUserDataFullInfo = z.object({
path: z.string(),
size: z.number(),
modified: z.number()
})
const zBookmarkCustomization = z.object({
icon: z.string().optional(),
color: z.string().optional()
Expand Down Expand Up @@ -506,3 +510,4 @@ export type DeviceStats = z.infer<typeof zDeviceStats>
export type SystemStats = z.infer<typeof zSystemStats>
export type User = z.infer<typeof zUser>
export type UserData = z.infer<typeof zUserData>
export type UserDataFullInfo = z.infer<typeof zUserDataFullInfo>
Loading

0 comments on commit 17db1e6

Please sign in to comment.