@teambit/objects 0.0.703 → 0.0.705
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/version.d.ts +5 -5
- package/dist/objects/object-list.d.ts +23 -0
- package/dist/objects/object-list.js +58 -12
- package/dist/objects/object-list.js.map +1 -1
- package/dist/objects/repository.d.ts +22 -3
- package/dist/objects/repository.js +99 -32
- package/dist/objects/repository.js.map +1 -1
- package/dist/objects/repository.spec.js +115 -0
- package/dist/objects/repository.spec.js.map +1 -1
- package/objects/object-list.ts +51 -14
- package/objects/repository.spec.ts +102 -0
- package/objects/repository.ts +85 -23
- package/package.json +11 -11
- /package/dist/{preview-1790963186429.js → preview-1790971258035.js} +0 -0
package/objects/object-list.ts
CHANGED
|
@@ -46,6 +46,16 @@ export const FETCH_FORMAT_OBJECT_LIST = 'ObjectList';
|
|
|
46
46
|
*/
|
|
47
47
|
export type ObjectItemsStream = Readable;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* compressed buffer of an object (as stored on the filesystem) and the size of its inflated content.
|
|
51
|
+
*/
|
|
52
|
+
export type CompressedObject = { buffer: Buffer; inflatedSize: number };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* hash => compressed object
|
|
56
|
+
*/
|
|
57
|
+
export type RawObjectsMap = Map<BitObject, CompressedObject & { ref: Ref }>;
|
|
58
|
+
|
|
49
59
|
export class ObjectList {
|
|
50
60
|
constructor(public objects: ObjectItem[] = []) {}
|
|
51
61
|
|
|
@@ -124,7 +134,10 @@ export class ObjectList {
|
|
|
124
134
|
resolve(objects);
|
|
125
135
|
});
|
|
126
136
|
|
|
127
|
-
|
|
137
|
+
// unlike `pipe`, it rejects when the input stream fails or the extractor fails (e.g. a truncated tar).
|
|
138
|
+
pipeline(packStream, extract, (err) => {
|
|
139
|
+
if (err) reject(err);
|
|
140
|
+
});
|
|
128
141
|
});
|
|
129
142
|
return new ObjectList(objectItems);
|
|
130
143
|
}
|
|
@@ -248,23 +261,46 @@ export class ObjectList {
|
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
addIfNotExist(objectItems: ObjectItem[]) {
|
|
264
|
+
const existing = new Set(this.objects.map((object) => ObjectList.combineScopeAndHash(object)));
|
|
251
265
|
objectItems.forEach((objectItem) => {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
this.objects.push(objectItem);
|
|
257
|
-
}
|
|
266
|
+
const key = ObjectList.combineScopeAndHash(objectItem);
|
|
267
|
+
if (existing.has(key)) return;
|
|
268
|
+
existing.add(key);
|
|
269
|
+
this.objects.push(objectItem);
|
|
258
270
|
});
|
|
259
271
|
}
|
|
260
272
|
|
|
261
273
|
async toBitObjects(throwForUnknownTypes = false): Promise<BitObjectList> {
|
|
274
|
+
const { bitObjectList } = await this.parseObjects(throwForUnknownTypes);
|
|
275
|
+
return bitObjectList;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* same as `toBitObjects`, but also returns the original compressed buffers of the Source objects (files/artifacts)
|
|
280
|
+
* whose content matches their ref. Sources are immutable, so these buffers can be written to the filesystem as is,
|
|
281
|
+
* saving the costly re-compression. (significant for components with thousands of files).
|
|
282
|
+
*/
|
|
283
|
+
async toBitObjectsWithRawSources(): Promise<{ bitObjectList: BitObjectList; rawSources: RawObjectsMap }> {
|
|
284
|
+
return this.parseObjects(false, true);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async parseObjects(
|
|
288
|
+
throwForUnknownTypes: boolean,
|
|
289
|
+
collectRawSources = false
|
|
290
|
+
): Promise<{ bitObjectList: BitObjectList; rawSources: RawObjectsMap }> {
|
|
262
291
|
const concurrency = concurrentIOLimit();
|
|
292
|
+
const rawSources: RawObjectsMap = new Map();
|
|
263
293
|
const bitObjects = await pMapPool(
|
|
264
294
|
this.objects,
|
|
265
295
|
async (object) => {
|
|
266
296
|
try {
|
|
267
|
-
|
|
297
|
+
const { object: bitObject, inflatedSize } = await BitObject.parseObjectWithSize(object.buffer);
|
|
298
|
+
if (collectRawSources && bitObject.getType() === 'Source') {
|
|
299
|
+
if (bitObject.hash().isEqual(object.ref)) {
|
|
300
|
+
rawSources.set(bitObject, { buffer: object.buffer, inflatedSize, ref: object.ref });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return bitObject;
|
|
268
304
|
} catch (err) {
|
|
269
305
|
if (throwForUnknownTypes || !(err instanceof UnknownObjectType)) {
|
|
270
306
|
throw err;
|
|
@@ -277,7 +313,7 @@ export class ObjectList {
|
|
|
277
313
|
},
|
|
278
314
|
{ concurrency }
|
|
279
315
|
);
|
|
280
|
-
return new BitObjectList(compact(bitObjects));
|
|
316
|
+
return { bitObjectList: new BitObjectList(compact(bitObjects)), rawSources };
|
|
281
317
|
}
|
|
282
318
|
|
|
283
319
|
static async fromBitObjects(bitObjects: BitObject[]): Promise<ObjectList> {
|
|
@@ -302,6 +338,7 @@ export class ObjectList {
|
|
|
302
338
|
|
|
303
339
|
splitByScopeName(): { [scopeName: string]: ObjectList } {
|
|
304
340
|
const objectListPerScope: { [scopeName: string]: ObjectList } = {};
|
|
341
|
+
const added = new Set<string>();
|
|
305
342
|
this.objects.forEach((obj) => {
|
|
306
343
|
if (obj.type === ExportMetadata.name) {
|
|
307
344
|
return; // no scope for this type. it's general for all export data from all scopes
|
|
@@ -309,11 +346,11 @@ export class ObjectList {
|
|
|
309
346
|
if (!obj.scope) {
|
|
310
347
|
throw new Error(`ObjectList: unable to split by scopeName, the scopeName is missing for ${obj.ref.hash}`);
|
|
311
348
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
349
|
+
const key = ObjectList.combineScopeAndHash(obj);
|
|
350
|
+
if (added.has(key)) return;
|
|
351
|
+
added.add(key);
|
|
352
|
+
if (!objectListPerScope[obj.scope]) objectListPerScope[obj.scope] = new ObjectList();
|
|
353
|
+
objectListPerScope[obj.scope].objects.push(obj);
|
|
317
354
|
});
|
|
318
355
|
return objectListPerScope;
|
|
319
356
|
}
|
|
@@ -2,9 +2,11 @@ import { expect } from 'chai';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { Readable } from 'stream';
|
|
5
6
|
import zlib from 'zlib';
|
|
6
7
|
import { ScopeJson } from '@teambit/legacy.scope';
|
|
7
8
|
import { ModelComponent, Source } from '../models';
|
|
9
|
+
import { ObjectList } from './object-list';
|
|
8
10
|
import Ref from './ref';
|
|
9
11
|
import Repository from './repository';
|
|
10
12
|
import { IndexType } from './scope-index';
|
|
@@ -90,3 +92,103 @@ describe('Repository.list', () => {
|
|
|
90
92
|
expect(objects[0]).to.be.instanceOf(ModelComponent);
|
|
91
93
|
});
|
|
92
94
|
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* the export flow moves every object through these methods, so for components with thousands of files, the number of
|
|
98
|
+
* fs operations and compressions per object is what makes an export fast or slow.
|
|
99
|
+
*/
|
|
100
|
+
describe('Repository writing objects for export', () => {
|
|
101
|
+
let scopePath: string;
|
|
102
|
+
let repository: Repository;
|
|
103
|
+
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
scopePath = await fs.mkdtemp(path.join(os.tmpdir(), 'bit-repository-spec-'));
|
|
106
|
+
const scopeJson = new ScopeJson(
|
|
107
|
+
{ name: SCOPE_NAME, version: '1.0.0', groupName: null },
|
|
108
|
+
path.join(scopePath, 'scope.json')
|
|
109
|
+
);
|
|
110
|
+
repository = await Repository.create({ scopePath, scopeJson });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(async () => {
|
|
114
|
+
await fs.remove(scopePath);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const createObjectList = async (count: number) => {
|
|
118
|
+
const sources = Array.from({ length: count }, (_, i) => Source.from(Buffer.from(`file ${i}`)));
|
|
119
|
+
return ObjectList.fromBitObjects(sources);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
it('should write a file per pending object and read them back', async () => {
|
|
123
|
+
const objectList = await createObjectList(SOURCES_COUNT);
|
|
124
|
+
const pendingDir = path.join(scopePath, 'pending-objects', 'client-1');
|
|
125
|
+
await repository.writeObjectsToPendingDir(objectList, pendingDir);
|
|
126
|
+
for (const objectItem of objectList.objects) {
|
|
127
|
+
const hash = objectItem.ref.toString();
|
|
128
|
+
const written = await fs.readFile(path.join(pendingDir, hash.slice(0, 2), hash.slice(2)));
|
|
129
|
+
expect(written.equals(objectItem.buffer)).to.be.true;
|
|
130
|
+
}
|
|
131
|
+
const loaded = await repository.readObjectsFromPendingDir(pendingDir);
|
|
132
|
+
expect(loaded.objects.map((o) => o.ref.toString()).sort()).to.deep.equal(
|
|
133
|
+
objectList.objects.map((o) => o.ref.toString()).sort()
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should write the given raw buffers of sources without re-compressing them, and leave no tmp files', async () => {
|
|
138
|
+
const objectList = await createObjectList(SOURCES_COUNT);
|
|
139
|
+
const { bitObjectList, rawSources } = await objectList.toBitObjectsWithRawSources();
|
|
140
|
+
expect(rawSources.size).to.equal(SOURCES_COUNT);
|
|
141
|
+
bitObjectList.getAll().forEach((obj) => {
|
|
142
|
+
obj.compressWithSize = async () => {
|
|
143
|
+
throw new Error('should not re-compress');
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
await repository.writeObjectsToTheFS(bitObjectList.getAll(), rawSources);
|
|
147
|
+
for (const objectItem of objectList.objects) {
|
|
148
|
+
const written = await fs.readFile(repository.objectPath(objectItem.ref));
|
|
149
|
+
expect(written.equals(objectItem.buffer)).to.be.true;
|
|
150
|
+
}
|
|
151
|
+
const objectDirs = await fs.readdir(repository.getPath());
|
|
152
|
+
const allFiles = (
|
|
153
|
+
await Promise.all(objectDirs.map((dir) => fs.readdir(path.join(repository.getPath(), dir))))
|
|
154
|
+
).flat();
|
|
155
|
+
expect(allFiles).to.have.lengthOf(SOURCES_COUNT);
|
|
156
|
+
expect(allFiles.filter((fileName) => fileName.startsWith('.'))).to.have.lengthOf(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should not use a raw buffer of a source whose content does not match its ref', async () => {
|
|
160
|
+
const [first, second] = (await createObjectList(2)).objects;
|
|
161
|
+
const tampered = new ObjectList([{ ref: first.ref, buffer: second.buffer }]);
|
|
162
|
+
const { rawSources } = await tampered.toBitObjectsWithRawSources();
|
|
163
|
+
expect(rawSources.size).to.equal(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should re-create an object dir that was removed after it was written to', async () => {
|
|
167
|
+
const source = Source.from(Buffer.from('some content'));
|
|
168
|
+
await repository.writeObjectsToTheFS([source]);
|
|
169
|
+
await fs.remove(path.dirname(repository.objectPath(source.hash())));
|
|
170
|
+
await repository.writeObjectsToTheFS([source]);
|
|
171
|
+
expect(await fs.pathExists(repository.objectPath(source.hash()))).to.be.true;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should keep the mode of an existing object file it overwrites, as write-file-atomic does', async () => {
|
|
175
|
+
const source = Source.from(Buffer.from('some content'));
|
|
176
|
+
await repository.writeObjectsToTheFS([source]);
|
|
177
|
+
const objectPath = repository.objectPath(source.hash());
|
|
178
|
+
await fs.chmod(objectPath, 0o640);
|
|
179
|
+
await repository.writeObjectsToTheFS([source]);
|
|
180
|
+
expect((await fs.stat(objectPath)).mode & 0o777).to.equal(0o640);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should reject reading a truncated tar of objects rather than hang', async () => {
|
|
184
|
+
const objectList = await createObjectList(SOURCES_COUNT);
|
|
185
|
+
const chunks: Buffer[] = [];
|
|
186
|
+
for await (const chunk of objectList.toTar() as AsyncIterable<Buffer>) chunks.push(chunk);
|
|
187
|
+
const truncated = Buffer.concat(chunks).subarray(0, 700);
|
|
188
|
+
let error: Error | undefined;
|
|
189
|
+
await ObjectList.fromTar(Readable.from([truncated])).catch((err) => {
|
|
190
|
+
error = err;
|
|
191
|
+
});
|
|
192
|
+
expect(error).to.be.instanceOf(Error);
|
|
193
|
+
});
|
|
194
|
+
});
|
package/objects/repository.ts
CHANGED
|
@@ -5,11 +5,11 @@ import { BitError } from '@teambit/bit-error';
|
|
|
5
5
|
import type { ComponentID } from '@teambit/component-id';
|
|
6
6
|
import { HASH_SIZE, isSnap } from '@teambit/component-version';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { userInfo } from 'os';
|
|
8
9
|
import { pMapPool } from '@teambit/toolbox.promise.map-pool';
|
|
9
10
|
import { OBJECTS_DIR } from '@teambit/legacy.constants';
|
|
10
11
|
import { logger } from '@teambit/legacy.logger';
|
|
11
|
-
import type {
|
|
12
|
-
import { writeFile } from '@teambit/legacy.utils';
|
|
12
|
+
import type { PathOsBasedAbsolute } from '@teambit/legacy.utils';
|
|
13
13
|
import { glob } from 'glob';
|
|
14
14
|
import { removeEmptyDir } from '@teambit/toolbox.fs.remove-empty-dir';
|
|
15
15
|
import { concurrentIOLimit } from '@teambit/harmony.modules.concurrency';
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import type { IndexType, IndexItem } from './scope-index';
|
|
25
25
|
import { ScopeIndex } from './scope-index';
|
|
26
26
|
import BitObject from './object';
|
|
27
|
-
import type { ObjectItem } from './object-list';
|
|
27
|
+
import type { ObjectItem, RawObjectsMap, CompressedObject } from './object-list';
|
|
28
28
|
import { ObjectList } from './object-list';
|
|
29
29
|
import BitRawObject from './raw-object';
|
|
30
30
|
import Ref from './ref';
|
|
@@ -37,6 +37,8 @@ type ContentTransformer = (content: Buffer) => Buffer;
|
|
|
37
37
|
const OBJECTS_BACKUP_DIR = `${OBJECTS_DIR}.bak`;
|
|
38
38
|
const TRASH_DIR = 'trash';
|
|
39
39
|
const MAX_COMPRESSED_SIZE_TO_CACHE = 100 * 1024; // don't cache big files (mainly artifacts) to prevent out-of-memory
|
|
40
|
+
let objectFileWriteCounter = 0;
|
|
41
|
+
type ObjectChownOptions = { uid: number; gid: number } | null;
|
|
40
42
|
/**
|
|
41
43
|
* how much of an object file to read when all that's needed is its header. the header holds the
|
|
42
44
|
* type and is a few dozen bytes, which 512 compressed bytes always cover - and every byte past it
|
|
@@ -790,14 +792,13 @@ export default class Repository {
|
|
|
790
792
|
/**
|
|
791
793
|
* write all objects to the FS and index the components/lanes/symlink objects
|
|
792
794
|
*/
|
|
793
|
-
async writeObjectsToTheFS(objects: BitObject[]): Promise<void> {
|
|
795
|
+
async writeObjectsToTheFS(objects: BitObject[], rawObjects?: RawObjectsMap): Promise<void> {
|
|
794
796
|
const count = objects.length;
|
|
795
797
|
if (!count) return;
|
|
796
798
|
logger.trace(`Repository.writeObjectsToTheFS: started writing ${count} objects`);
|
|
797
799
|
const concurrency = concurrentIOLimit();
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
});
|
|
800
|
+
const chownOptions = await this.getChownOptions();
|
|
801
|
+
await pMapPool(objects, (obj) => this._writeOne(obj, rawObjects?.get(obj), chownOptions), { concurrency });
|
|
801
802
|
logger.trace(`Repository.writeObjectsToTheFS: completed writing ${count} objects`);
|
|
802
803
|
|
|
803
804
|
const added = this.scopeIndex.addMany(objects);
|
|
@@ -820,16 +821,21 @@ export default class Repository {
|
|
|
820
821
|
* this method doesn't write to scopeIndex. so using this method for ModelComponent or
|
|
821
822
|
* Symlink makes the index outdated.
|
|
822
823
|
*/
|
|
823
|
-
async _writeOne(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
824
|
+
async _writeOne(
|
|
825
|
+
object: BitObject,
|
|
826
|
+
raw?: CompressedObject & { ref: Ref },
|
|
827
|
+
chownOptions?: ObjectChownOptions
|
|
828
|
+
): Promise<void> {
|
|
829
|
+
// when the already-compressed buffer of an unchanged object is available, skip the costly re-compression.
|
|
830
|
+
const { buffer: contents, inflatedSize } = raw || (await object.compressWithSize());
|
|
831
|
+
const options = chownOptions === undefined ? await this.getChownOptions() : chownOptions;
|
|
832
|
+
// hashing a Source is sha1 of its content. the raw buffer's ref was already checked against it.
|
|
833
|
+
const hash = raw?.ref || object.hash();
|
|
828
834
|
const objectPath = this.objectPath(hash);
|
|
829
835
|
logger.trace(`repository._writeOne: ${objectPath}`);
|
|
830
836
|
// Run hook to transform content pre persisting
|
|
831
837
|
const transformedContent = this.onPersist(contents);
|
|
832
|
-
await
|
|
838
|
+
await this.writeObjectFile(objectPath, transformedContent, options);
|
|
833
839
|
// once written, the object is the up-to-date one. this also replaces the size-estimate of objects that
|
|
834
840
|
// were cached by `add()`.
|
|
835
841
|
if (this.cache.has(hash.toString())) this.cache.set(hash.toString(), object, inflatedSize);
|
|
@@ -850,27 +856,73 @@ export default class Repository {
|
|
|
850
856
|
const ref = object.ref.toString();
|
|
851
857
|
if (!isSnap(ref)) throw new BitError(`invalid object ref: ${JSON.stringify(ref)}`);
|
|
852
858
|
}
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
await writeFile(objPath, object.buffer, options);
|
|
859
|
-
})
|
|
859
|
+
const chownOptions = await this.getChownOptions();
|
|
860
|
+
await pMapPool(
|
|
861
|
+
objectList.objects,
|
|
862
|
+
(object) => this.writeObjectFile(path.join(pendingDir, this.hashPath(object.ref)), object.buffer, chownOptions),
|
|
863
|
+
{ concurrency: concurrentIOLimit() }
|
|
860
864
|
);
|
|
861
865
|
}
|
|
862
866
|
|
|
863
867
|
async readObjectsFromPendingDir(pendingDir: PathOsBasedAbsolute) {
|
|
864
868
|
const refs = await this.listRefs(pendingDir);
|
|
865
|
-
const objects = await
|
|
866
|
-
refs
|
|
869
|
+
const objects = await pMapPool(
|
|
870
|
+
refs,
|
|
871
|
+
async (ref) => {
|
|
867
872
|
const buffer = await fs.readFile(path.join(pendingDir, this.hashPath(ref)));
|
|
868
873
|
return { ref, buffer };
|
|
869
|
-
}
|
|
874
|
+
},
|
|
875
|
+
{ concurrency: concurrentIOLimit() }
|
|
870
876
|
);
|
|
871
877
|
return new ObjectList(objects);
|
|
872
878
|
}
|
|
873
879
|
|
|
880
|
+
/**
|
|
881
|
+
* the owner to set on written object files. `null` when the scope has no group (or it can't be resolved), in which
|
|
882
|
+
* case the owner of an existing file is kept.
|
|
883
|
+
*/
|
|
884
|
+
private async getChownOptions(): Promise<ObjectChownOptions> {
|
|
885
|
+
if (!this.scopeJson.groupName) return null;
|
|
886
|
+
const gid = await resolveGroupId(this.scopeJson.groupName);
|
|
887
|
+
return gid ? { uid: userInfo().uid, gid } : null;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* writes the file atomically (tmp file + rename), the same way `writeFile` of legacy.utils does with write-file-atomic,
|
|
892
|
+
* including keeping the mode (and owner, when no chown is given) of an existing file and ignoring the same chown/chmod
|
|
893
|
+
* errors. it's called for every object, so it saves the fs calls write-file-atomic and `ensureDir` do on every write
|
|
894
|
+
* (realpath, stat of the dir, exit handlers). the parent dir is created only when the first write fails with ENOENT.
|
|
895
|
+
* the tmp file is a dotfile, which `listRefs()` ignores in case it's left behind.
|
|
896
|
+
*/
|
|
897
|
+
private async writeObjectFile(filePath: string, contents: Buffer, chownOptions: ObjectChownOptions) {
|
|
898
|
+
const existing = await fs.stat(filePath).catch(() => undefined);
|
|
899
|
+
const mode = existing?.mode;
|
|
900
|
+
// process.getuid is undefined on Windows, where there's no owner to keep.
|
|
901
|
+
const keepOwner =
|
|
902
|
+
existing && typeof process.getuid === 'function' ? { uid: existing.uid, gid: existing.gid } : null;
|
|
903
|
+
const chown = chownOptions || keepOwner;
|
|
904
|
+
objectFileWriteCounter += 1;
|
|
905
|
+
const tmpPath = path.join(
|
|
906
|
+
path.dirname(filePath),
|
|
907
|
+
`.${path.basename(filePath)}.${process.pid}.${objectFileWriteCounter}`
|
|
908
|
+
);
|
|
909
|
+
try {
|
|
910
|
+
try {
|
|
911
|
+
await fs.writeFile(tmpPath, contents, { mode });
|
|
912
|
+
} catch (err: any) {
|
|
913
|
+
if (err.code !== 'ENOENT') throw err;
|
|
914
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
915
|
+
await fs.writeFile(tmpPath, contents, { mode });
|
|
916
|
+
}
|
|
917
|
+
if (chown) await fs.chown(tmpPath, chown.uid, chown.gid).catch(throwUnlessChownErrOk);
|
|
918
|
+
if (mode) await fs.chmod(tmpPath, mode).catch(throwUnlessChownErrOk);
|
|
919
|
+
await fs.rename(tmpPath, filePath);
|
|
920
|
+
} catch (err) {
|
|
921
|
+
await fs.remove(tmpPath).catch(() => {});
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
874
926
|
private hashPath(ref: Ref) {
|
|
875
927
|
const hash = ref.toString();
|
|
876
928
|
return path.join(hash.slice(0, 2), hash.slice(2));
|
|
@@ -893,6 +945,16 @@ async function removeFile(filePath: string, propagateDirs = false): Promise<bool
|
|
|
893
945
|
return true;
|
|
894
946
|
}
|
|
895
947
|
|
|
948
|
+
/**
|
|
949
|
+
* same as write-file-atomic: a non-root user can't always chown/chmod, and it's not an error.
|
|
950
|
+
*/
|
|
951
|
+
function throwUnlessChownErrOk(err: any) {
|
|
952
|
+
if (err.code === 'ENOSYS') return;
|
|
953
|
+
const nonRoot = !process.getuid || process.getuid() !== 0;
|
|
954
|
+
if (nonRoot && (err.code === 'EINVAL' || err.code === 'EPERM')) return;
|
|
955
|
+
throw err;
|
|
956
|
+
}
|
|
957
|
+
|
|
896
958
|
async function resolveGroupId(groupName: string): Promise<number | null | undefined> {
|
|
897
959
|
// unix group ids are meaningless on Windows, which has no /etc/group and no process.getgid
|
|
898
960
|
if (!process.getgid) return null;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teambit/objects",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.705",
|
|
4
4
|
"homepage": "https://bit.cloud/teambit/scope/objects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"componentId": {
|
|
7
7
|
"scope": "teambit.scope",
|
|
8
8
|
"name": "objects",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.705"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"lodash": "4.17.21",
|
|
@@ -21,11 +21,9 @@
|
|
|
21
21
|
"@teambit/harmony": "0.4.12",
|
|
22
22
|
"@teambit/harmony.modules.runtimes": "0.0.1",
|
|
23
23
|
"@teambit/component-id": "1.2.4",
|
|
24
|
-
"@teambit/component.snap-distance": "0.0.174",
|
|
25
24
|
"@teambit/graph.cleargraph": "0.0.11",
|
|
26
25
|
"@teambit/harmony.modules.concurrency": "0.0.47",
|
|
27
26
|
"@teambit/legacy.logger": "0.0.63",
|
|
28
|
-
"@teambit/legacy.scope": "0.0.173",
|
|
29
27
|
"@teambit/toolbox.promise.map-pool": "0.0.21",
|
|
30
28
|
"@teambit/legacy.constants": "0.0.46",
|
|
31
29
|
"@teambit/legacy.utils": "0.0.52",
|
|
@@ -35,19 +33,21 @@
|
|
|
35
33
|
"@teambit/harmony.modules.in-memory-cache": "0.0.51",
|
|
36
34
|
"@teambit/toolbox.fs.remove-empty-dir": "0.0.20",
|
|
37
35
|
"@teambit/lane-id": "0.0.312",
|
|
38
|
-
"@teambit/harmony.modules.get-basic-log": "0.0.174",
|
|
39
|
-
"@teambit/config-store": "0.0.285",
|
|
40
36
|
"@teambit/legacy.cli.error": "0.0.63",
|
|
41
|
-
"@teambit/component.sources": "0.0.225",
|
|
42
37
|
"@teambit/legacy-bit-id": "1.1.3",
|
|
43
38
|
"@teambit/legacy-component-log": "0.0.432",
|
|
44
|
-
"@teambit/legacy.consumer-component": "0.0.174",
|
|
45
|
-
"@teambit/legacy.consumer-config": "0.0.173",
|
|
46
|
-
"@teambit/legacy.extension-data": "0.0.175",
|
|
47
39
|
"@teambit/pkg.modules.semver-helper": "0.0.36",
|
|
48
40
|
"@teambit/toolbox.array.duplications-finder": "0.0.16",
|
|
49
41
|
"@teambit/bit.get-bit-version": "0.0.29",
|
|
50
|
-
"@teambit/
|
|
42
|
+
"@teambit/component.snap-distance": "0.0.176",
|
|
43
|
+
"@teambit/legacy.scope": "0.0.175",
|
|
44
|
+
"@teambit/harmony.modules.get-basic-log": "0.0.176",
|
|
45
|
+
"@teambit/config-store": "0.0.287",
|
|
46
|
+
"@teambit/component.sources": "0.0.227",
|
|
47
|
+
"@teambit/legacy.consumer-component": "0.0.176",
|
|
48
|
+
"@teambit/legacy.consumer-config": "0.0.175",
|
|
49
|
+
"@teambit/legacy.extension-data": "0.0.177",
|
|
50
|
+
"@teambit/semantics.doc-parser": "0.0.183"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/lodash": "4.14.165",
|
|
File without changes
|