mirror of https://github.com/wekan/wekan
The Open Source kanban (built with Meteor). Keep variable/table/field names camelCase. For translations, only add Pull Request changes to wekan/i18n/en.i18n.json , other translations are done at https://transifex.com/wekan/wekan only.
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
5.7 KiB
174 lines
5.7 KiB
import { Meteor } from 'meteor/meteor';
|
|
import { MongoInternals } from 'meteor/mongo';
|
|
import { Random } from 'meteor/random';
|
|
|
|
/**
|
|
* Read/write/delete helpers for the legacy CollectionFS GridFS layout, used by
|
|
* the unified attachment/avatar storage migration.
|
|
*
|
|
* Verified structure (from a real WeKan 6.09 mongorestore):
|
|
*
|
|
* cfs.<coll>.filerecord (metadata, _id = Meteor random id)
|
|
* { _id, original:{name,size,type},
|
|
* boardId, swimlaneId, listId, cardId, userId, uploadedAt, // top level
|
|
* copies: { <coll>: { name, type, size, key:<gridfs file _id hex>,
|
|
* updatedAt, createdAt } } }
|
|
*
|
|
* cfs_gridfs.<coll>.files / .chunks (GridFS bucket; file _id = copies.<coll>.key)
|
|
* files: { _id:ObjectId, filename, contentType, length, chunkSize, uploadDate, md5 }
|
|
*
|
|
* `coll` is 'attachments' or 'avatars'.
|
|
*/
|
|
|
|
function getDb() {
|
|
const db = MongoInternals.defaultRemoteCollectionDriver()?.mongo?.db;
|
|
if (!db) {
|
|
throw new Meteor.Error('mongo-unavailable', 'MongoDB connection is not available');
|
|
}
|
|
return db;
|
|
}
|
|
|
|
function getBucket(db, coll) {
|
|
const { GridFSBucket } = MongoInternals.NpmModule;
|
|
return new GridFSBucket(db, { bucketName: `cfs_gridfs.${coll}` });
|
|
}
|
|
|
|
function toObjectId(hexOrId) {
|
|
const { ObjectId } = MongoInternals.NpmModule;
|
|
if (hexOrId instanceof ObjectId) return hexOrId;
|
|
return new ObjectId(String(hexOrId));
|
|
}
|
|
|
|
function filerecordCollName(coll) {
|
|
return `cfs.${coll}.filerecord`;
|
|
}
|
|
|
|
// Normalize a filerecord into the common shape the migration engine consumes.
|
|
export function normalizeCollectionFsRecord(coll, rec) {
|
|
const copy = (rec.copies && rec.copies[coll]) || {};
|
|
const key = copy.key;
|
|
return {
|
|
backend: 'collectionfs',
|
|
coll,
|
|
sourceId: rec._id,
|
|
gridFsKey: key,
|
|
name: (rec.original && rec.original.name) || copy.name || rec._id,
|
|
type: (rec.original && rec.original.type) || copy.type || 'application/octet-stream',
|
|
size: (rec.original && rec.original.size) || copy.size || 0,
|
|
// CollectionFS keeps board/card ids at the top level; Meteor-Files nests
|
|
// them under meta. Carry them in a normalized `meta` plus the owner userId.
|
|
meta: {
|
|
boardId: rec.boardId,
|
|
swimlaneId: rec.swimlaneId,
|
|
listId: rec.listId,
|
|
cardId: rec.cardId,
|
|
},
|
|
userId: rec.userId,
|
|
uploadedAt: rec.uploadedAt || (copy.createdAt) || new Date(),
|
|
};
|
|
}
|
|
|
|
// List all CollectionFS records for a collection ('attachments' | 'avatars').
|
|
export async function listCollectionFsRecords(coll) {
|
|
if (!Meteor.isServer) return [];
|
|
const db = getDb();
|
|
let recs = [];
|
|
try {
|
|
recs = await db.collection(filerecordCollName(coll)).find({}).toArray();
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
return recs
|
|
.map(rec => normalizeCollectionFsRecord(coll, rec))
|
|
.filter(r => r.gridFsKey); // only records whose binary key is present
|
|
}
|
|
|
|
export async function countCollectionFsRecords(coll) {
|
|
if (!Meteor.isServer) return 0;
|
|
const db = getDb();
|
|
try {
|
|
return await db.collection(filerecordCollName(coll)).countDocuments({});
|
|
} catch (error) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Read the binary for a normalized CollectionFS record into a Buffer.
|
|
export async function readCollectionFsBuffer(item) {
|
|
const db = getDb();
|
|
const bucket = getBucket(db, item.coll);
|
|
const gridFsId = toObjectId(item.gridFsKey);
|
|
return await new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
const stream = bucket.openDownloadStream(gridFsId);
|
|
stream.on('data', c => chunks.push(c));
|
|
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
stream.on('error', reject);
|
|
});
|
|
}
|
|
|
|
// Write a buffer into the CollectionFS layout (GridFS bucket + filerecord),
|
|
// reproducing the genuine old-WeKan structure. `info` carries the normalized
|
|
// fields (name, type, size, meta{boardId,...}, userId, uploadedAt).
|
|
export async function writeCollectionFsRecord(coll, info, buffer) {
|
|
const db = getDb();
|
|
const bucket = getBucket(db, coll);
|
|
|
|
// 1. Write the binary; the GridFS file _id becomes the filerecord "key".
|
|
const gridFsId = await new Promise((resolve, reject) => {
|
|
const uploadStream = bucket.openUploadStream(info.name, {
|
|
contentType: info.type,
|
|
});
|
|
uploadStream.on('error', reject);
|
|
uploadStream.on('finish', () => resolve(uploadStream.id));
|
|
uploadStream.end(buffer);
|
|
});
|
|
|
|
const now = info.uploadedAt || new Date();
|
|
const recordId = Random.id();
|
|
const filerecord = {
|
|
_id: recordId,
|
|
original: {
|
|
name: info.name,
|
|
size: info.size || (buffer ? buffer.length : 0),
|
|
type: info.type,
|
|
updatedAt: now,
|
|
},
|
|
boardId: info.meta && info.meta.boardId,
|
|
swimlaneId: info.meta && info.meta.swimlaneId,
|
|
listId: info.meta && info.meta.listId,
|
|
cardId: info.meta && info.meta.cardId,
|
|
userId: info.userId,
|
|
uploadedAt: now,
|
|
copies: {
|
|
[coll]: {
|
|
name: info.name,
|
|
type: info.type,
|
|
size: info.size || (buffer ? buffer.length : 0),
|
|
key: gridFsId.toString(),
|
|
updatedAt: now,
|
|
createdAt: now,
|
|
},
|
|
},
|
|
};
|
|
await db.collection(filerecordCollName(coll)).insertOne(filerecord);
|
|
return { sourceId: recordId, gridFsKey: gridFsId.toString() };
|
|
}
|
|
|
|
// Delete a CollectionFS record's filerecord and its GridFS binary.
|
|
export async function deleteCollectionFsRecord(coll, sourceId, gridFsKey) {
|
|
const db = getDb();
|
|
try {
|
|
await db.collection(filerecordCollName(coll)).deleteOne({ _id: sourceId });
|
|
} catch (error) {
|
|
console.error('[collectionFsStore] Failed to delete filerecord', sourceId, error);
|
|
}
|
|
if (gridFsKey) {
|
|
try {
|
|
const bucket = getBucket(db, coll);
|
|
await bucket.delete(toObjectId(gridFsKey));
|
|
} catch (error) {
|
|
// file/chunks may already be gone
|
|
}
|
|
}
|
|
}
|
|
|