Skip to content

Commit 07b3a7d

Browse files
committed
support attachments
Signed-off-by: Anna Khismatullina <[email protected]>
1 parent 772b82c commit 07b3a7d

File tree

9 files changed

+155
-74
lines changed

9 files changed

+155
-74
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,7 @@
839839
"-pw",
840840
"1234",
841841
"-ws",
842-
"ws4"
842+
"ws1"
843843
],
844844
"env": {
845845
"FRONT_URL": "http://localhost:8087"

dev/import-tool/docs/huly/example-workspace/SlaveCard.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class: card:class:MasterTag
2-
title: Slave Card 11
2+
title: Slave Card 1
33
properties:
44
- label: sex
55
type: TypeBoolean

dev/import-tool/docs/huly/example-workspace/SlaveCard/Frodo.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ color: pink
99
x: from familiar
1010
y: from minion
1111
multy-enum: [Beta]
12+
attachments:
13+
- ../../CARDS_INSTRUCTIONS.md
14+
- ../files/screenshot.png
15+
# blobs:
16+
# - ../README.md
1217
---
1318

1419
A brave hobbit guided by Gandalf.

packages/importer/src/huly/huly.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { type FileUploader } from '../importer/uploader'
6868
import { UnifiedDoc } from '../types'
6969
import { readMarkdownContent, readYamlHeader } from './parsing'
7070
import { UnifiedDocProcessor } from './unified'
71+
import attachment from '@hcengineering/model-attachment'
7172

7273
export interface HulyComment {
7374
author: string
@@ -490,7 +491,7 @@ export class HulyFormatImporter {
490491
}
491492

492493
// Импортируем UnifiedDoc сущности
493-
const { docs: unifiedDocs, mixins: unifiedMixins } = await this.unifiedDocImporter.importFromDirectory(folderPath)
494+
const { docs: unifiedDocs, mixins: unifiedMixins, files } = await this.unifiedDocImporter.importFromDirectory(folderPath)
494495

495496
// Разбираем и добавляем в билдер по классу
496497
for (const [path, docs] of unifiedDocs.entries()) {
@@ -514,6 +515,9 @@ export class HulyFormatImporter {
514515
case core.class.Enum:
515516
builder.addEnum(path, doc as UnifiedDoc<Enum>)
516517
break
518+
case attachment.class.Attachment:
519+
builder.addAttachment(path, doc as UnifiedDoc<Attachment>)
520+
break
517521
default:
518522
if (isId(doc._class) || (doc._class as string).startsWith('card:types:')) { // todo: fix system cards validation
519523
builder.addCard(path, doc as UnifiedDoc<Card>)
@@ -532,6 +536,10 @@ export class HulyFormatImporter {
532536
}
533537
}
534538

539+
for (const [path, file] of files.entries()) {
540+
builder.addFile(path, file)
541+
}
542+
535543
// Process all yaml files first
536544
const yamlFiles = fs.readdirSync(folderPath).filter((f) => f.endsWith('.yaml') && f !== 'settings.yaml')
537545

packages/importer/src/huly/metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface TagMetadata {
1717
}
1818

1919
export class MetadataStorage {
20-
private readonly pathToRef = new Map<string, Ref<Doc>>()
20+
private readonly pathToRef = new Map<string, Ref<Doc>>() // todo: attachments to a separate map?
2121
private readonly pathToMetadata = new Map<string, TagMetadata>()
2222

2323
public getIdByFullPath (path: string): Ref<Doc> {

packages/importer/src/huly/unified.ts

Lines changed: 55 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// unified.ts
2+
import { Attachment } from '@hcengineering/attachment'
23
import card, { Card, MasterTag, Tag } from '@hcengineering/card'
34
import core, {
45
Association,
56
Attribute,
7+
Blob as PlatformBlob,
8+
Class,
69
Doc,
710
Enum,
811
generateId,
@@ -11,15 +14,17 @@ import core, {
1114
} from '@hcengineering/core'
1215
import * as fs from 'fs'
1316
import * as yaml from 'js-yaml'
17+
import { contentType } from 'mime-types'
1418
import * as path from 'path'
1519
import { IntlString } from '../../../platform/types'
16-
import { Props, UnifiedDoc, UnifiedMixin } from '../types'
20+
import { Props, UnifiedDoc, UnifiedFile, UnifiedMixin } from '../types'
1721
import { MetadataStorage, RelationMetadata } from './metadata'
1822
import { readMarkdownContent, readYamlHeader } from './parsing'
1923

2024
export interface UnifiedDocProcessResult {
2125
docs: Map<string, Array<UnifiedDoc<Doc>>>
2226
mixins: Map<string, Array<UnifiedMixin<Doc, Doc>>>
27+
files: Map<string, UnifiedFile>
2328
}
2429

2530
export class UnifiedDocProcessor {
@@ -28,7 +33,8 @@ export class UnifiedDocProcessor {
2833
async importFromDirectory (directoryPath: string): Promise<UnifiedDocProcessResult> {
2934
const result: UnifiedDocProcessResult = {
3035
docs: new Map(),
31-
mixins: new Map()
36+
mixins: new Map(),
37+
files: new Map()
3238
}
3339
// Первый проход - собираем метаданные
3440
await this.processMetadata(directoryPath, result)
@@ -137,68 +143,6 @@ export class UnifiedDocProcessor {
137143
}
138144
}
139145

140-
// private async processDirectory (
141-
// currentPath: string,
142-
// result: UnifiedDocProcessResult,
143-
// parentMasterTagId?: Ref<MasterTag>,
144-
// parentMasterTagAttrs?: Map<string, UnifiedDoc<Attribute<MasterTag>>>
145-
// ): Promise<void> {
146-
// const entries = fs.readdirSync(currentPath, { withFileTypes: true })
147-
148-
// // Сначала обрабатываем YAML файлы (потенциальные мастер-теги)
149-
// for (const entry of entries) {
150-
// if (!entry.isFile() || !entry.name.endsWith('.yaml')) continue // todo: filter entries by extension
151-
152-
// const yamlPath = path.resolve(currentPath, entry.name)
153-
// const yamlConfig = yaml.load(fs.readFileSync(yamlPath, 'utf8')) as Record<string, any>
154-
155-
// switch (yamlConfig?.class) {
156-
// case card.class.MasterTag: {
157-
// const masterTagId = this.metadataStorage.getIdByFullPath(yamlPath) as Ref<MasterTag>
158-
// const masterTag = await this.createMasterTag(yamlConfig, masterTagId, parentMasterTagId)
159-
160-
// const masterTagAttrs = await this.createAttributes(yamlConfig, masterTagId)
161-
// this.metadataStorage.setAttributes(yamlPath, masterTagAttrs)
162-
163-
// const docs = result.docs.get(yamlPath) ?? []
164-
// docs.push(
165-
// masterTag,
166-
// ...Array.from(masterTagAttrs.values())
167-
// )
168-
// result.docs.set(yamlPath, docs)
169-
170-
// const masterTagDir = path.join(currentPath, path.basename(yamlPath, '.yaml'))
171-
// if (fs.existsSync(masterTagDir) && fs.statSync(masterTagDir).isDirectory()) {
172-
// await this.processDirectory(masterTagDir, result, masterTagId, masterTagAttrs)
173-
// }
174-
// break
175-
// }
176-
// case card.class.Tag: {
177-
// if (parentMasterTagId === undefined) {
178-
// throw new Error('Tag should be inside master tag folder: ' + currentPath)
179-
// }
180-
181-
// await this.processTag(yamlPath, yamlConfig, result, parentMasterTagId)
182-
// break
183-
// }
184-
// case core.class.Association: {
185-
// const association = await this.createAssociation(yamlPath, yamlConfig)
186-
// result.docs.set(yamlPath, [association])
187-
// break
188-
// }
189-
// default:
190-
// throw new Error('Unsupported class: ' + yamlConfig?.class) // todo: handle default case just convert to UnifiedDoc
191-
// }
192-
// }
193-
194-
// if (parentMasterTagId === undefined || parentMasterTagAttrs === undefined) {
195-
// await this.processSystemCards(currentPath, result)
196-
// } else {
197-
// // await this.processCardDirectory(result, currentPath, parentMasterTagId, parentMasterTagAttrs) // todo: handle parent master tag attrs
198-
// await this.processCardDirectory(result, currentPath, parentMasterTagId)
199-
// }
200-
// }
201-
202146
private async processSystemCards (
203147
currentDir: string,
204148
result: UnifiedDocProcessResult,
@@ -239,6 +183,9 @@ export class UnifiedDocProcessor {
239183
const card = cardWithRelations[0] as UnifiedDoc<Card>
240184
await this.applyTags(card, cardProps, cardPath, result)
241185

186+
const attachments = cardProps.attachments ?? []
187+
await this.processAttachments(attachments, cardPath, card, result)
188+
242189
// Проверяем наличие дочерних карточек
243190
const cardDir = path.join(path.dirname(cardPath), path.basename(cardPath, '.md'))
244191
if (fs.existsSync(cardDir) && fs.statSync(cardDir).isDirectory()) {
@@ -563,6 +510,50 @@ export class UnifiedDocProcessor {
563510
}
564511
}
565512

513+
private async processAttachments (
514+
attachments: string[],
515+
cardPath: string,
516+
card: UnifiedDoc<Card>,
517+
result: UnifiedDocProcessResult
518+
): Promise<void> {
519+
for (const attachment of attachments) {
520+
const attachmentPath = path.resolve(path.dirname(cardPath), attachment)
521+
const attachmentName = path.basename(attachmentPath)
522+
const fileId = this.metadataStorage.getIdByFullPath(attachmentPath) as Ref<PlatformBlob>
523+
const type = contentType(attachmentPath)
524+
const size = fs.statSync(attachmentPath).size
525+
526+
const file: UnifiedFile = {
527+
_id: fileId, // id for datastore
528+
name: attachmentName,
529+
blobProvider: async () => {
530+
const data = fs.readFileSync(attachmentPath)
531+
const props = type !== false ? { type } : undefined
532+
return new Blob([data], props)
533+
}
534+
}
535+
result.files.set(attachmentPath, file)
536+
537+
const attachmentId = this.metadataStorage.getIdByFullPath(attachmentPath) as Ref<Attachment>
538+
const attachmentDoc: UnifiedDoc<Attachment> = {
539+
_class: 'attachment:class:Attachment' as Ref<Class<Attachment>>,
540+
props: {
541+
_id: attachmentId, // id for attachment doc
542+
space: core.space.Workspace,
543+
attachedTo: card.props._id as Ref<Card>,
544+
attachedToClass: card._class,
545+
file: fileId,
546+
name: attachmentName,
547+
collection: 'attachments',
548+
lastModified: Date.now(),
549+
type: type !== false ? type : 'application/octet-stream',
550+
size
551+
}
552+
}
553+
result.docs.set(attachmentPath, [attachmentDoc])
554+
}
555+
}
556+
566557
private async createAssociation (
567558
yamlPath: string,
568559
yamlConfig: Record<string, any>

packages/importer/src/importer/builder.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
type ImportTeamspace,
3030
type ImportWorkspace
3131
} from './importer'
32-
import { UnifiedDoc, UnifiedMixin } from '../types'
32+
import { UnifiedDoc, UnifiedFile, UnifiedMixin } from '../types'
33+
import { Attachment } from '@hcengineering/attachment'
3334

3435
export interface ValidationError {
3536
path: string
@@ -65,7 +66,9 @@ export class ImportWorkspaceBuilder {
6566
private readonly associations = new Map<string, UnifiedDoc<Association>>()
6667
private readonly relations = new Map<string, UnifiedDoc<Relation>>()
6768
private readonly enums = new Map<string, UnifiedDoc<Enum>>()
69+
private readonly attachments = new Map<string, UnifiedDoc<Attachment>>()
6870
private readonly mixins = new Map<string, UnifiedMixin<Doc, Doc>>()
71+
private readonly files = new Map<string, UnifiedFile>()
6972

7073
private readonly projectTypes = new Map<string, ImportProjectType>()
7174
private readonly issueStatusCache = new Map<string, Ref<IssueStatus>>()
@@ -270,11 +273,21 @@ export class ImportWorkspaceBuilder {
270273
return this
271274
}
272275

276+
addAttachment (path: string, attachment: UnifiedDoc<Attachment>): this {
277+
this.validateAndAdd('attachment', path, attachment, (a) => this.validateAttachment(a), this.attachments, path)
278+
return this
279+
}
280+
273281
addTagMixin (path: string, mixin: UnifiedMixin<Doc, Doc>): this {
274282
this.validateAndAdd('tagMixin', path, mixin, (m) => this.validateTagMixin(m), this.mixins, path + '/' + mixin.mixin) // todo: fix mixin key
275283
return this
276284
}
277285

286+
addFile (path: string, file: UnifiedFile): this {
287+
this.validateAndAdd('file', path, file, (f) => this.validateFile(f), this.files, path)
288+
return this
289+
}
290+
278291
validate (): ValidationResult {
279292
// Perform cross-entity validation
280293
this.validateSpacesReferences()
@@ -352,9 +365,11 @@ export class ImportWorkspaceBuilder {
352365
...Array.from(this.cards.values()),
353366
...Array.from(this.associations.values()),
354367
...Array.from(this.relations.values()),
355-
...Array.from(this.enums.values())
368+
...Array.from(this.enums.values()),
369+
...Array.from(this.attachments.values())
356370
],
357371
mixins: Array.from(this.mixins.values()),
372+
files: Array.from(this.files.values()),
358373
attachments: []
359374
}
360375
}
@@ -929,6 +944,14 @@ export class ImportWorkspaceBuilder {
929944
return errors
930945
}
931946

947+
private validateAttachment (attachment: UnifiedDoc<Attachment>): string[] {
948+
const errors: string[] = []
949+
950+
// todo: validate attachment
951+
952+
return errors
953+
}
954+
932955
private validateCard (card: UnifiedDoc<Card>): string[] {
933956
const errors: string[] = []
934957

@@ -1001,6 +1024,14 @@ export class ImportWorkspaceBuilder {
10011024
return errors
10021025
}
10031026

1027+
private validateFile (file: UnifiedFile): string[] {
1028+
const errors: string[] = []
1029+
1030+
// todo: validate file
1031+
1032+
return errors
1033+
}
1034+
10041035
private validateOrgSpace (space: ImportOrgSpace): string[] {
10051036
const errors: string[] = []
10061037

0 commit comments

Comments
 (0)