Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
011e77c1c6 wip 2024-10-29 15:44:08 +01:00
891 changed files with 12750 additions and 28856 deletions

View File

@@ -105,8 +105,6 @@ module.exports = {
],
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'no-void': ['error', { allowAsStatement: true }],
'n/no-missing-import': 'off', // using 'import' plugin instead, to support TS aliases
'no-redeclare': 'off', // automatically checked by the TypeScript compiler
@@ -227,7 +225,7 @@ module.exports = {
'@intlify/vue-i18n/no-v-html': 'error',
'@intlify/vue-i18n/valid-message-syntax': 'error',
'@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error',
'@intlify/vue-i18n/no-missing-keys-in-other-locales': ['error', { ignoreLocales: ['de', 'fa', 'cs'] }],
'@intlify/vue-i18n/no-missing-keys-in-other-locales': ['error', { ignoreLocales: ['de', 'fa'] }],
},
},
// Specific rules for XO dev-related files
@@ -263,8 +261,6 @@ module.exports = {
},
],
ignorePatterns: ['@vates/fatfs/'],
parserOptions: {
ecmaVersion: 13,
sourceType: 'script',

View File

@@ -25,7 +25,7 @@
"dependencies": {
"@vates/multi-key-map": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/log": "^0.7.0",
"ensure-array": "^1.0.0"
},
"devDependencies": {

View File

@@ -1,3 +0,0 @@
.DS_Store
.vscode/
node_modules/

View File

@@ -1,127 +0,0 @@
# @vates/fatfs
This is a fork of the original library [natevw/fatfs](https://github.com/natevw/fatfs).
## Why?
This fork was created to add a missing feature in the original library: the ability to create labels (createLabel).
The original library did not support this functionality, which was required to make Cloudbase-Init work on windows VMs.
See: https://github.com/natevw/fatfs/issues/30, https://github.com/natevw/fatfs/pull/31
A standalone FAT16/FAT32 implementation that takes in a block-access interface and exposes something quite similar to `require('fs')` (i.e. the node.js built-in [Filesystem API](http://nodejs.org/api/fs.html)).
## Installation
`npm install fatfs`
## Example
```js
var fatfs = require('fatfs'),
fs = fatfs.createFileSystem(exampleDriver) // see below
fs.stat('autoexec.bat', function (e, stats) {
if (e) console.error(e)
else console.log(stats)
})
// TODO: open a file and write to it or something…
```
## API
- `fs = fatfs.createFileSystem(vol, [opts], [cb])` — Simply pass in a block driver (see below) mapped to a FAT partition somewhere, and get back the API documented [here](http://nodejs.org/api/fs.html). An options dictionary can be provided, details are in the next section. You may also optionally provide a callback `cb(err)` which will be automatically registered for the on `'ready'` or `'error'` event.
- `'ready'` event — fired on `fs` when initial volume information has been determined and the API is ready to use. It is safe to call other `fs` methods before this fires **only if** you are sure the first sector will be readable and represents a valid FAT volume.
- `'error'` event — fired if initialization fails for whatever reason.
### Filesystem options
The `opts` dictionary you pass to `fatfs.createFileSystem` can contain any of the following options:
- `ro` — Enables readonly mode if `true`. It defaults to `false`, but if your volume driver does not provide a `writeSectors` method it will be overriden to `true`.
- `noatime` — The FAT filesystem can track the last access time (just a date, actually) but this means every read operation would also incur some write overhead. Defaults to `true`, meaning by default access times will **not** be updated on reads. Set this to `false` to track access times.
- `modmode` — chooses how `fs.chmod` (and the mode field from `fs.stat`–family calls) should map FAT attributes to POSIX permissions. Set to the number `0111` to map the readonly flag to the user's write bit being unset, and the archive/system/hidden flags to the user/group/other executable bits respectively. Set to the number `07000` to map the readonly flag to _all_ write bits being unset, and the archive/system/hidden flags to the sticky/setgid/setuid bits respectively. Set to `null` for readonly mapping. Defaults to `0111`.
- `umask` — any bits _set_ in this octal number will be _unset_ in the 'mode' field from `fs.stat`–family calls. It does not affect anything else. Defaults to `process.umask()`, or `0022` if that is unavailable.
- `uid` — This value will be returned as the 'uid' stat field. It does not affect anything else. Defaults to `process.getuid()`, or `0` if that is unavailable.
- `gid` — This value will be returned as the 'gid' stat field. It does not affect anything else. Defaults to `process.getgid()`, or `0` if that is unavailable.
(Note that these are similar to the options you could use with a POSIX `mount` operation.)
And that's it! The [rest of the API](http://nodejs.org/api/fs.html) (`fs.readdir`, `fs.open`, `fs.createReadStream`, `fs.appendFile`, etc.) is as documented by the node.js project.
Well, sort of…
## Caveats
### Temporary
- **BETA** **BETA** **BETA**. Seriously, this is a _brand new_, _from scratch_, _completely unproven_ filesystem implementation. It does not have full automated test coverage, and it has not been manually tested very much either. Please please please **make sure you have a backup** of any important drive/image/card you unleash this upon.
- A few methods are not quite implemented, either: `fs.rename`, `fs.unlink` and `fs.rmdir`, as well as `fs.watchFile`/`fs.unwatchFile` and `fs.watch`. These are Coming Soon™.
- There are several internal housekeeping items (redundant FAT tables, extra FAT32 information, etc.) that are not done. These do not seem to affect interop, but you may see warnings when repairing a filesystem written by this module.
- Oh, and not to scare you, but if an IO error happens while writing, the library usually just bails — bubbling an error up to your callback as if it were a hot potato. Although some attempt has been made to do separate writes in the safest order (e.g. allocating an additional file cluster, then appending data into it, and then finally updating the file's size), but this behavior has not been thoroughly audited for all operations. There's certainly no attempt to retry/cleanup/rollback if a multi-step change runs into trouble partway through.
### As-planned
Some of the differences between `fatfs` and the node.js `fs` module are "by design" for architectural simplicity and/or due to underlying FAT limitations.
- There are no `fs.*Sync` methods. (The volume driver is async, not to mention that supporting a separate \*Sync codepath would be an enormous duplication of effort of dubious value.)
- This module does [almost] no read/write caching. This should be done in your volume driver, but see notes below.
- You'll need multiple `createFileSystem` instances for multiple volumes; paths are relative to each, and don't share a namespace.
- The FAT filesystem has no concept of symlinks, and hardlinks are not really an intentional feature. You will get an ENOSYS-like error when trying to create either type of link.
## "Volume driver" API
To use 'fatfs', you must provide a driver object with the following properties/methods:
- `driver.sectorSize` — number of bytes per sector on this device
- `driver.numSectors` — count of sectors available on this media
- `driver.readSectors(i, dest, cb)` — Fill `dest` with data starting at the `i`th sector and notify `cb(e)` when finished. You may assume `dest.length` is a multiple of `driver.sectorSize`.
- `driver.writeSectors(i, data, cb)` — (optional) Write `data` starting at the `i`th sector and notify `cb(e)` when finished. You may assume `data.length` is a multiple of `driver.sectorSize`.
If you do not provide a `writeSectors` method, then `fatfs` will work in readonly mode. Pretty simple, eh? And the 'fatfs' module makes a good effort to check the parameters passed to your driver methods!
**TBD:** to facilitate proper cache handling, this module might add an optional `driver.flush(cb)` method at some point in the future.
Here's an example taken from code used to run this module's own tests:
```js
// NOTE: this assumes image at `path` has no partition table.
// If it did, you'd need to translate positions, natch…
var fs = require('fs')
exports.createDriverSync = function (path, opts) {
opts || (opts = {})
var secSize = 512,
ro = opts.readOnly || false,
fd = fs.openSync(path, ro ? 'r' : 'r+'),
s = fs.fstatSync(fd)
return {
sectorSize: secSize,
numSectors: s.size / secSize,
readSectors: function (i, dest, cb) {
if (dest.length % secSize) throw Error('Unexpected buffer length!')
fs.read(fd, dest, 0, dest.length, i * secSize, function (e, n, d) {
cb(e, d)
})
},
writeSectors: ro
? null
: function (i, data, cb) {
if (data.length % secSize) throw Error('Unexpected buffer length!')
fs.write(fd, data, 0, data.length, i * secSize, function (e) {
cb(e)
})
},
}
}
```
## License
© 2014 Nathan Vander Wilt.
Funding for this work was provided by Technical Machine, Inc.
Reuse under your choice of:
- [BSD-2-Clause](http://opensource.org/licenses/BSD-2-Clause)
- [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html)

View File

@@ -1,60 +0,0 @@
var _ = require("./helpers.js");
exports.wrapDriver = function (volume, opts) {
opts = _.extend({
maxSectors: 2048
}, opts);
var cache = {},
advice = 'NORMAL',
secSize = volume.sectorSize;
function _freezeBuffer(b) {
var f = _.allocBuffer(b.length);
b.copy(f);
return f;
}
function addToCache(i, data) {
if (advice === 'SEQUENTIAL' || advice === 'NOREUSE') return;
data = _freezeBuffer(data);
cache[i] = data;
//if (data.length > secSize) addToCache(i+1, data.slice(secSize));
while (data.length > secSize) {
data = data.slice(secSize);
cache[++i] = data;
}
// simple highest-sectors-lose eviction policy for now
Object.keys(cache).sort().slice(opts.maxSectors).forEach(function (x) {
delete cache[x];
});
_.log(_.log.DBG, "Cache now contains:", Object.keys(cache).join(','));
}
return {
sectorSize: volume.sectorSize,
numSectors: volume.numSectors,
advice: function (val) {
if (!arguments.length) return advice;
else advice = val;
if (advice === 'SEQUENTIAL' || advice === 'NOREUSE') cache = {};
return this;
},
readSectors: function (i, dest, cb) {
// TODO: handle having partial parts of dest!
if (i in cache && dest.length === secSize) {
cache[i].copy(dest);
setImmediate(cb);
} else volume.readSectors(i, dest, function (e) {
if (e) cb(e);
else addToCache(i, dest), cb();
});
},
writeSectors: (!volume.writeSectors) ? null : function (i, data, cb) {
volume.writeSectors(i, data, function (e) {
if (e) cb(e);
else addToCache(i, data), cb();
});
}
};
};

View File

@@ -1,287 +0,0 @@
var S = require("./structs.js"),
_ = require("./helpers.js");
function _baseChain(vol) {
var chain = {};
chain.sectorSize = vol._sectorSize;
function posFromOffset(off) {
var secSize = chain.sectorSize,
offset = off % secSize,
sector = (off - offset) / secSize;
return {sector:sector, offset:offset};
}
var sectorCache = vol._makeCache();
Object.defineProperty(chain, 'cacheAdvice', {
enumerable: true,
get: function () { return sectorCache.advice(); },
set: function (v) { sectorCache.advice(v); }
});
chain._vol_readSectors = vol._readSectors.bind(vol, sectorCache);
chain._vol_writeSectors = vol._writeSectors.bind(vol, sectorCache);
// cb(error, bytesRead, buffer)
chain.readFromPosition = function (targetPos, buffer, cb) {
if (typeof targetPos === 'number') targetPos = posFromOffset(targetPos);
if (typeof buffer === 'number') buffer = _.allocBuffer(buffer);
/* NOTE: to keep our contract with the volume driver, we need to read on _full_ sector boundaries!
So we divide the read into [up to] three parts: {preface, main, trailer}
This is kind of unfortunate, but in practice should often still be reasonably efficient. */
if (targetPos.offset) chain.readSectors(targetPos.sector, _.allocBuffer(chain.sectorSize), function (e,d) {
if (e || !d) cb(e, 0, buffer);
else { // copy preface into `buffer`
var dBeg = targetPos.offset,
dEnd = dBeg + buffer.length;
d.copy(buffer, 0, dBeg, dEnd);
if (dEnd > d.length) readMain();
else cb(null, buffer.length, buffer);
}
}); else readMain();
function readMain() {
var prefaceLen = targetPos.offset && (chain.sectorSize - targetPos.offset),
trailerLen = (buffer.length - prefaceLen) % chain.sectorSize,
mainSector = (prefaceLen) ? targetPos.sector + 1 : targetPos.sector,
mainBuffer = (trailerLen) ? buffer.slice(prefaceLen, -trailerLen) : buffer.slice(prefaceLen);
if (mainBuffer.length) chain.readSectors(mainSector, mainBuffer, function (e,d) {
if (e || !d) cb(e, prefaceLen, buffer);
else if (!trailerLen) cb(null, buffer.length, buffer);
else readTrailer();
}); else readTrailer();
function readTrailer() {
var trailerSector = mainSector + (mainBuffer.length / chain.sectorSize);
chain.readSectors(trailerSector, _.allocBuffer(chain.sectorSize), function (e,d) {
if (e || !d) cb(e, buffer.length-trailerLen, buffer);
else {
d.copy(buffer, buffer.length-trailerLen, 0, trailerLen);
cb(null, buffer.length, buffer);
}
});
}
}
};
// cb(error)
chain.writeToPosition = function (targetPos, data, cb) {
_.log(_.log.DBG, "WRITING", data.length, "bytes at", targetPos, "in", this.toJSON(), data);
if (typeof targetPos === 'number') targetPos = posFromOffset(targetPos);
var prefaceBuffer = (targetPos.offset) ? data.slice(0, chain.sectorSize-targetPos.offset) : null;
if (prefaceBuffer) _modifySector(targetPos.sector, targetPos.offset, prefaceBuffer, function (e) {
if (e) cb(e);
else if (prefaceBuffer.length < data.length) writeMain();
else cb();
}); else writeMain();
function writeMain() {
var prefaceLen = (prefaceBuffer) ? prefaceBuffer.length : 0,
trailerLen = (data.length - prefaceLen) % chain.sectorSize,
mainSector = (prefaceLen) ? targetPos.sector + 1 : targetPos.sector,
mainBuffer = (trailerLen) ? data.slice(prefaceLen, -trailerLen) : data.slice(prefaceLen);
if (mainBuffer.length) chain.writeSectors(mainSector, mainBuffer, function (e) {
if (e) cb(e);
else if (!trailerLen) cb();
else writeTrailer();
}); else writeTrailer();
function writeTrailer() {
var trailerSector = mainSector + (mainBuffer.length / chain.sectorSize),
trailerBuffer = data.slice(data.length-trailerLen); // WORKAROUND: https://github.com/tessel/runtime/issues/721
_modifySector(trailerSector, 0, trailerBuffer, cb);
}
}
function _modifySector(sec, off, data, cb) {
chain.readSectors(sec, _.allocBuffer(chain.sectorSize), function (e, orig) {
if (e) return cb(e);
orig || (orig = _.allocBuffer(chain.sectorSize, 0));
data.copy(orig, off);
chain.writeSectors(sec, orig, cb);
});
}
};
return chain;
};
exports.clusterChain = function (vol, firstCluster, _parent) {
var chain = _baseChain(vol),
cache = [firstCluster];
chain.firstCluster = firstCluster;
function _cacheIsComplete() {
return cache[cache.length-1] === 'eof';
}
function extendCacheToInclude(i, cb) { // NOTE: may `cb()` before returning!
if (i < cache.length) cb(null, cache[i]);
else if (_cacheIsComplete()) cb(null, 'eof');
else vol.fetchFromFAT(cache[cache.length-1], function (e,d) {
if (e) cb(e);
else if (typeof d === 'string' && d !== 'eof') cb(S.err.IO());
else {
cache.push(d);
extendCacheToInclude(i, cb);
}
});
}
function expandChainToLength(clusterCount, cb) {
if (!_cacheIsComplete()) throw Error("Must be called only when cache is complete!");
else cache.pop(); // remove 'eof' entry until finished
function addCluster(clustersNeeded, lastCluster) {
if (!clustersNeeded) cache.push('eof'), cb();
else vol.allocateInFAT(lastCluster, function (e, newCluster) {
if (e) cb(e);
else vol.storeToFAT(lastCluster, newCluster, function (e) {
if (e) return cb(e);
cache.push(newCluster);
addCluster(clustersNeeded-1, newCluster);
});
});
}
addCluster(clusterCount - cache.length, cache[cache.length - 1]);
}
function shrinkChainToLength(clusterCount, cb) {
if (!_cacheIsComplete()) throw Error("Must be called only when cache is complete!");
else cache.pop(); // remove 'eof' entry until finished
function removeClusters(count, cb) {
if (!count) cache.push('eof'), cb();
else vol.storeToFAT(cache.pop(), 'free', function (e) {
if (e) cb(e);
else removeClusters(count - 1, cb);
});
}
// NOTE: for now, we don't remove the firstCluster ourselves; we should though!
if (clusterCount) removeClusters(cache.length - clusterCount, cb);
else removeClusters(cache.length - 1, cb);
}
// [{firstSector,numSectors},{firstSector,numSectors},…]
function determineSectorGroups(sectorIdx, numSectors, alloc, cb) {
var sectorOffset = sectorIdx % vol._sectorsPerCluster,
clusterIdx = (sectorIdx - sectorOffset) / vol._sectorsPerCluster,
numClusters = Math.ceil((numSectors + sectorOffset) / vol._sectorsPerCluster),
chainLength = clusterIdx + numClusters;
extendCacheToInclude(chainLength-1, function (e,c) {
if (e) cb(e);
else if (c === 'eof' && alloc) expandChainToLength(chainLength, function (e) {
if (e) cb(e);
else _determineSectorGroups();
});
else _determineSectorGroups();
});
function _determineSectorGroups() {
// …now we have a complete cache
var groups = [],
_group = null;
for (var i = clusterIdx; i < chainLength; ++i) {
var c = (i < cache.length) ? cache[i] : 'eof';
if (c === 'eof') break;
else if (_group && c !== _group._nextCluster) {
groups.push(_group);
_group = null;
}
if (!_group) _group = {
_nextCluster: c+1,
firstSector: vol._firstSectorOfCluster(c) + sectorOffset,
numSectors: vol._sectorsPerCluster - sectorOffset
}; else {
_group._nextCluster += 1;
_group.numSectors += vol._sectorsPerCluster;
}
sectorOffset = 0; // only first group is offset
}
if (_group) groups.push(_group);
cb(null, groups, i === chainLength);
}
}
chain.readSectors = function (i, dest, cb) {
var groupOffset = 0, groupsPending;
determineSectorGroups(i, dest.length / chain.sectorSize, false, function (e, groups, complete) {
if (e) cb(e);
else if (!complete) groupsPending = -1, _pastEOF(cb);
else if ((groupsPending = groups.length)) groups.forEach(function (group) {
var groupLength = group.numSectors * chain.sectorSize,
groupBuffer = dest.slice(groupOffset, groupOffset += groupLength);
chain._vol_readSectors(group.firstSector, groupBuffer, function (e,d) {
if (e && groupsPending !== -1) groupsPending = -1, cb(e);
else if (--groupsPending === 0) cb(null, dest);
});
});
else cb(null, dest); // 0-length destination case
});
};
// TODO: does this handle NOSPC condition?
chain.writeSectors = function (i, data, cb) {
var groupOffset = 0, groupsPending;
determineSectorGroups(i, data.length / chain.sectorSize, true, function (e, groups) {
if (e) cb(e);
else if ((groupsPending = groups.length)) groups.forEach(function (group) {
var groupLength = group.numSectors * chain.sectorSize,
groupBuffer = data.slice(groupOffset, groupOffset += groupLength);
chain._vol_writeSectors(group.firstSector, groupBuffer, function (e) {
if (e && groupsPending !== -1) groupsPending = -1, cb(e);
else if (--groupsPending === 0) cb();
});
});
else cb(); // 0-length data case
});
};
chain.truncate = function (numSectors, cb) {
extendCacheToInclude(Infinity, function (e,c) {
if (e) return cb(e);
var currentLength = cache.length-1,
clustersNeeded = Math.ceil(numSectors / vol._sectorsPerCluster);
if (clustersNeeded < currentLength) shrinkChainToLength(clustersNeeded, cb);
else if (clustersNeeded > currentLength) expandChainToLength(clustersNeeded, cb);
else cb();
});
};
chain.toJSON = function () {
return {firstCluster:firstCluster};
};
return chain;
};
exports.sectorChain = function (vol, firstSector, numSectors) {
var chain = _baseChain(vol);
chain.firstSector = firstSector;
chain.numSectors = numSectors;
chain.readSectors = function (i, dest, cb) {
if (i < numSectors) chain._vol_readSectors(firstSector+i, dest, cb);
else _pastEOF(cb);
};
chain.writeSectors = function (i, data, cb) {
if (i < numSectors) chain._vol_writeSectors(firstSector+i, data, cb);
else _.delayedCall(cb, S.err.NOSPC());
};
chain.truncate = function (i, cb) {
_.delayedCall(cb, S.err.INVAL());
};
chain.toJSON = function () {
return {firstSector:firstSector, numSectors:numSectors};
};
return chain;
};
// NOTE: used with mixed feelings, broken out to mark uses
function _pastEOF(cb) { _.delayedCall(cb, null, null); }

View File

@@ -1,372 +0,0 @@
var S = require("./structs.js"),
_ = require("./helpers.js");
var dir = exports;
dir.iterator = function (dirChain, opts) {
opts || (opts = {});
var cache = {buffer:null, n: null};
function getSectorBuffer(n, cb) {
if (cache.n === n) cb(null, cache.buffer);
else cache.n = cache.buffer = null, dirChain.readSectors(n, _.allocBuffer(dirChain.sectorSize), function (e,d) {
if (e) cb(e);
else if (!d) return cb(null, null);
else {
cache.n = n;
cache.buffer = d;
getSectorBuffer(n, cb);
}
});
}
var secIdx = 0,
off = {bytes:0},
long = null;
function getNextEntry(cb) {
if (off.bytes >= dirChain.sectorSize) {
secIdx += 1;
off.bytes -= dirChain.sectorSize;
}
var entryPos = {chain:dirChain, sector:secIdx, offset:off.bytes};
getSectorBuffer(secIdx, function (e, buf) {
if (e) return cb(S.err.IO());
else if (!buf) return cb(null, null, entryPos);
var entryIdx = off.bytes,
signalByte = buf[entryIdx];
if (signalByte === S.entryDoneFlag) return cb(null, null, entryPos);
else if (signalByte === S.entryFreeFlag) {
off.bytes += S.dirEntry.size;
long = null;
if (opts.includeFree) return cb(null, {_free:true,_pos:entryPos}, entryPos);
else return getNextEntry(cb); // usually just skip these
}
var attrByte = buf[entryIdx+S.dirEntry.fields.Attr.offset],
entryType = (attrByte === S.longDirFlag) ? S.longDirEntry : S.dirEntry_simple;
var entry = entryType.valueFromBytes(buf, off);
entry._pos = entryPos;
_.log(_.log.DBG, "entry:", entry, secIdx, entryIdx);
if (entryType === S.longDirEntry) {
var firstEntry;
if (entry.Ord & S.lastLongFlag) {
firstEntry = true;
entry.Ord &= ~S.lastLongFlag;
long = {
name: -1,
sum: entry.Chksum,
_rem: entry.Ord-1,
_arr: []
}
}
if (firstEntry || long && entry.Chksum === long.sum && entry.Ord === long._rem--) {
var S_lde_f = S.longDirEntry.fields,
namepart = entry.Name1;
if (entry.Name1.length === S_lde_f.Name1.size/2) {
namepart += entry.Name2;
if (entry.Name2.length === S_lde_f.Name2.size/2) {
namepart += entry.Name3;
}
}
long._arr.push(namepart);
if (!long._rem) {
long.name = long._arr.reverse().join('');
delete long._arr;
delete long._rem;
}
} else long = null;
} else if ((attrByte & 0x08) === 0) { // NOTE: checks `!entry.Attr.volume_id`
var bestName = null;
if (long && long.name) {
var pos = entryIdx + S.dirEntry.fields['Name'].offset,
sum = _.checksumName(buf, pos);
if (sum === long.sum) bestName = long.name;
}
if (!bestName) {
if (signalByte === S.entryIsE5Flag) entry.Name.filename = '\u00E5'+entry.Name.filename.slice(1);
var nam = entry.Name.filename.replace(/ +$/, ''),
ext = entry.Name.extension.replace(/ +$/, '');
// TODO: lowercase bits http://en.wikipedia.org/wiki/8.3_filename#Compatibility
// via NTRes, bits 0x08 and 0x10 http://www.fdos.org/kernel/fatplus.txt.1
bestName = (ext) ? nam+'.'+ext : nam;
}
entry._name = bestName;
// OPTIMIZATION: avoid processing any fields for non-matching entries
// TODO: we could make this automatic via getters, but…?
var _entryBuffer = buf.slice(off.bytes-S.dirEntry.size, off.bytes);
entry._full = function () {
var _entry = S.dirEntry.valueFromBytes(_entryBuffer);
_.extend(entry, _entry);
entry._size = entry.FileSize;
entry._firstCluster = (entry.FstClusHI << 16) + entry.FstClusLO;
return entry;
};
long = null;
return cb(null, entry, entryPos);
} else long = null;
getNextEntry(cb);
});
}
function iter(cb) {
getNextEntry(cb);
return iter; // TODO: previous value can't be re-used, so why make caller re-assign?
}
return iter;
};
function _updateEntry(vol, entry, newStats) {
if ('size' in newStats) entry._size = entry.FileSize = newStats.size;
if ('_touch' in newStats) newStats.archive = newStats.atime = newStats.mtime = true;
if ('archive' in newStats) entry.Attr.archive = true; // TODO: also via newStats.mode?
var _now;
function applyDate(d, prefix, timeToo, tenthToo) {
if (d === true) d = _now || (_now = new Date());
entry[prefix+'Date'] = {year:d.getFullYear()-1980, month:d.getMonth()+1, day:d.getDate()};
if (timeToo) {
entry[prefix+'Time'] = {hours:d.getHours(), minutes:d.getMinutes(), seconds_2:d.getSeconds()>>>1};
if (tenthToo) {
var msec = (d.getSeconds() % 2)*1000 + d.getMilliseconds();
entry[prefix+'TimeTenth'] = Math.floor(msec / 100);
}
}
}
if ('ctime' in newStats) applyDate(newStats.ctime, 'Crt', true, true);
if ('mtime' in newStats) applyDate(newStats.mtime, 'Wrt', true);
if ('atime' in newStats) applyDate(newStats.atime, 'LstAcc');
if ('mode' in newStats) {
entry.Attr.directory = (newStats.mode & S._I.FDIR) ? true : false;
entry.Attr.volume_id = (newStats.mode & S._I.FREG) ? false : true;
if (vol.opts.modmode === 0111) {
entry.Attr.archive = (newStats.mode & S._I.XUSR) ? true : false;
entry.Attr.system = (newStats.mode & S._I.XGRP) ? true : false;
entry.Attr.hidden = (newStats.mode & S._I.XOTH) ? true : false;
entry.Attr.readonly = (newStats.mode & S._I.WUSR) ? false : true;
} else if (vol.opts.modmode === 07000) {
entry.Attr.archive = (newStats.mode & S._I.SVTX) ? true : false;
entry.Attr.system = (newStats.mode & S._I.SGID) ? true : false;
entry.Attr.hidden = (newStats.mode & S._I.SUID) ? true : false;
entry.Attr.readonly = (
newStats.mode & S._I.WUSR ||
newStats.mode & S._I.WGRP ||
newStats.mode & S._I.WOTH
) ? false : true;
}
}
if ('firstCluster' in newStats) {
entry.FstClusLO = newStats.firstCluster & 0xFFFF;
entry.FstClusHI = newStats.firstCluster >>> 16;
entry._firstCluster = newStats.firstCluster;
}
return entry;
}
dir.makeStat = function (vol, entry) {
var stats = {}; // TODO: return an actual `instanceof fs.Stat` somehow?
stats.isFile = function () {
return (!entry.Attr.volume_id && !entry.Attr.directory);
};
stats.isDirectory = function () {
return entry.Attr.directory;
};
stats.isBlockDevice = function () { return false; }
stats.isCharacterDevice = function () { return false; }
stats.isSymbolicLink = function () { return false; }
stats.isFIFO = function () { return false; }
stats.isSocket = function () { return false; }
stats.size = entry.FileSize;
stats.blksize = vol._sectorsPerCluster*vol._sectorSize;
stats.blocks = Math.ceil(stats.size / stats.blksize) || 1;
stats.nlink = 1;
stats.mode = S._I.RUSR | S._I.RGRP | S._I.ROTH;
if (!entry.Attr.readonly) stats.mode |= S._I.WUSR | S._I.WGRP | S._I.WOTH;
if (entry.Attr.directory) stats.mode |= S._I.FDIR;
else if (!entry.Attr.volume_id) stats.mode |= S._I.FREG;
// NOTE: discussion at https://github.com/natevw/fatfs/issues/7
if (vol.opts.modmode === 0111) {
// expose using executable bits, like Samba
if (entry.Attr.archive) stats.mode |= S._I.XUSR;
if (entry.Attr.system) stats.mode |= S._I.XGRP;
if (entry.Attr.hidden) stats.mode |= S._I.XOTH;
} else if (vol.opts.modmode === 07000) {
// expose using setXid/sticky bits, like MKS
if (entry.Attr.archive) stats.mode |= S._I.SVTX;
if (entry.Attr.system) stats.mode |= S._I.SGID;
if (entry.Attr.hidden) stats.mode |= S._I.SUID;
}
stats.mode &= ~vol.opts.umask;
stats.uid = vol.opts.uid;
stats.gid = vol.opts.gid;
function extractDate(prefix) {
var date = entry[prefix+'Date'],
time = entry[prefix+'Time'] || {hours:0, minutes:0, seconds_2:0},
secs = time.seconds_2 * 2,
sect = entry[prefix+'TimeTenth'] || 0;
if (sect > 100) {
secs += 1;
sect -= 100;
}
return new Date(date.year+1980, date.month-1, date.day, time.hours, time.minutes, secs, sect*100);
}
stats.atime = extractDate('LstAcc');
stats.mtime = extractDate('Wrt');
stats.ctime = extractDate('Crt');
entry = { // keep immutable copy (with only the fields we need)
Attr: _.extend({},entry.Attr),
};
return stats;
};
dir.init = function (vol, dirInfo, cb) {
var dirChain = dirInfo.chain,
isRootDir = ('numSectors' in dirChain), // HACK: all others would be a clusterChain
initialCluster = _.allocBuffer(dirChain.sectorSize*vol._sectorsPerCluster),
entriesOffset = {bytes:0};
initialCluster.fill(0);
function writeEntry(name, clusterNum) {
while (name.length < 8) name += " ";
S.dirEntry.bytesFromValue(_updateEntry(vol, {
Name: {filename:name, extension:" "},
Attr: {directory:true}
}, {firstCluster:clusterNum, _touch:true,ctime:true}), initialCluster, entriesOffset);
}
if (!isRootDir) {
writeEntry(".", dirChain.firstCluster);
writeEntry("..", dirInfo.parent.chain.firstCluster);
};
dirChain.writeToPosition(0, initialCluster, cb);
};
dir.addFile = function (vol, dirChain, entryInfo, opts, cb) {
if (typeof opts === 'function') {
cb = opts;
opts = {};
}
var name = entryInfo.name,
entries = [], mainEntry;
entries.push(mainEntry = {
Name: _.shortname(name),
Attr: {directory:opts.dir||false},
_name: name
});
if (1 || mainEntry.Name._lossy) { // HACK: always write long names until `._lossy` is more useful!
var workaroundTessel427 = ('\uFFFF'.length !== 1);
if (workaroundTessel427) throw Error("Your JS runtime does not have proper Unicode string support. (If Tessel, is your firmware up-to-date?)");
// name entries should be 0x0000-terminated and 0xFFFF-filled
var S_lde_f = S.longDirEntry.fields,
ENTRY_CHUNK_LEN = (S_lde_f.Name1.size + S_lde_f.Name2.size + S_lde_f.Name3.size)/2,
partialLen = name.length % ENTRY_CHUNK_LEN,
paddingNeeded = partialLen && (ENTRY_CHUNK_LEN - partialLen);
if (paddingNeeded--) name += '\u0000';
while (paddingNeeded-- > 0) name += '\uFFFF';
// now fill in as many entries as it takes
var off = 0,
ord = 1;
while (off < name.length) entries.push({
Ord: ord++,
Name1: name.slice(off, off+=S_lde_f.Name1.size/2),
Attr_raw: S.longDirFlag,
Chksum: null,
Name2: name.slice(off, off+=S_lde_f.Name2.size/2),
Name3: name.slice(off, off+=S_lde_f.Name3.size/2)
});
entries[entries.length - 1].Ord |= S.lastLongFlag;
}
if (entryInfo.tail) {
var name = mainEntry.Name.filename,
suffix = '~'+entryInfo.tail,
endIdx = name.indexOf(' '),
sufIdx = (~endIdx) ? Math.min(endIdx, name.length-suffix.length) : name.length-suffix.length;
if (sufIdx < 0) return cb(S.err.NAMETOOLONG()); // TODO: would EXIST be more correct?
mainEntry.Name.filename = name.slice(0,sufIdx)+suffix+name.slice(sufIdx+suffix.length);
_.log(_.log.DBG, "Shortname amended to:", mainEntry.Name);
}
vol.allocateInFAT(dirChain.firstCluster || 2, function (e,fileCluster) {
if (e) return cb(e);
var nameBuf = S.dirEntry.fields['Name'].bytesFromValue(mainEntry.Name),
nameSum = _.checksumName(nameBuf);
// TODO: finalize initial properties… (via `opts.mode` instead?)
_updateEntry(vol, mainEntry, {firstCluster:fileCluster, size:0, ctime:true,_touch:true});
mainEntry._pos = _.adjustedPos(vol, entryInfo.target, S.dirEntry.size*(entries.length-1));
entries.slice(1).forEach(function (entry) {
entry.Chksum = nameSum;
});
entries.reverse();
if (entryInfo.lastEntry) entries.push({});
var entriesData = _.allocBuffer(S.dirEntry.size*entries.length),
dataOffset = {bytes:0};
entries.forEach(function (entry) {
var entryType = ('Ord' in entry) ? S.longDirEntry : S.dirEntry;
entryType.bytesFromValue(entry, entriesData, dataOffset);
});
_.log(_.log.DBG, "Writing", entriesData.length, "byte directory entry", mainEntry, "into", dirChain.toJSON(), "at", entryInfo.target);
dirChain.writeToPosition(entryInfo.target, entriesData, function (e) {
// TODO: if we get error, what/should we clean up?
if (e) cb(e);
else cb(null, mainEntry, vol.chainForCluster(fileCluster, dirChain));
});
});
};
dir.findInDirectory = function (vol, dirChain, name, opts, cb) {
var matchName = name.toUpperCase(),
tailName = (opts.prepareForCreate) ? _.shortname(name) : null,
maxTail = 0;
function processNext(next) {
next = next(function (e, d, entryPos) {
if (e) cb(e);
else if (!d) cb(S.err.NOENT(), {tail:maxTail, target:entryPos, lastEntry:true});
else if (d._free) processNext(next); // TODO: look for long enough reusable run
else if (d._name.toUpperCase() === matchName) return cb(null, d._full());
else if (!opts.prepareForCreate) processNext(next);
else {
var dNum = 1,
dName = d.Name.filename,
dTail = dName.match(/(.*)~(\d+)/);
if (dTail) {
dNum = +dTail[2];
dName = dTail[1];
}
if (tailName.extension === d.Name.extension &&
tailName.filename.indexOf(dName) === 0)
{
maxTail = Math.max(dNum+1, maxTail);
}
processNext(next);
}
});
}
processNext(dir.iterator(dirChain, {includeFree:(0 && opts.prepareForCreate)}));
};
dir.updateEntry = function (vol, entry, newStats, cb) {
if (!entry._pos || !entry._pos.chain) throw Error("Entry source unknown!");
var entryPos = entry._pos,
newEntry = _updateEntry(vol, entry, newStats),
data = S.dirEntry.bytesFromValue(newEntry);
_.log(_.log.DBG, "UPDATING ENTRY", newStats, newEntry, entryPos, data);
// TODO: if write fails, then entry becomes corrupt!
entryPos.chain.writeToPosition(entryPos, data, cb);
};

View File

@@ -1,206 +0,0 @@
var S = require("./structs.js"),
_xok = require('xok');
// ponyfills for older node.js
exports.allocBuffer = Buffer.alloc || function (len, val) {
var b = Buffer(len);
if (arguments.length > 1) b.fill(val);
return b;
};
exports.bufferFrom = Buffer.from || function (arg0, arg1) {
return (arguments.length > 1) ? Buffer(arg0, arg1) : Buffer(arg0);
};
// flag for WORKAROUND: https://github.com/tessel/beta/issues/380
exports.workaroundTessel380 = !Buffer.from && function () {
var b = Buffer([0]),
s = b.slice(0);
return ((s[0] = 0xFF) !== b[0]);
}();
// WORKAROUND: https://github.com/tessel/beta/issues/433
var oldslice;
if (!Buffer.alloc && Buffer(5).slice(10).length < 0) oldslice = Buffer.prototype.slice, Buffer.prototype.slice = function (s, e) {
if (s > this.length) s = this.length;
// ~WORKAROUND: https://github.com/tessel/beta/issues/434
return (arguments.length > 1) ? oldslice.call(this, s, e) : oldslice.call(this, s);
}
exports.absoluteSteps = function (path) {
var steps = [];
path.split('/').forEach(function (str) {
// NOTE: these should actually be fine, just wasteful…
if (str === '..') steps.pop();
else if (str && str !== '.') steps.push(str);
});
return steps.map(exports.longname);
};
exports.absolutePath = function (path) {
return '/'+exports.absoluteSteps(path).join('/');
};
exports.parseFlags = function (flags) {
// read, write, append, create, truncate, exclusive
var info, _dir; // NOTE: there might be more clever ways to "parse", but…
if (flags[0] === '\\') {
// internal flag used internally to `fs.open` directories without `S.err.ISDIR()`
flags = flags.slice(1);
_dir = true;
}
switch (flags) {
case 'r': info = {read:true, write:false, create:false}; break;
case 'r+': info = {read:true, write:true, create:false}; break;
case 'rs': info = {read:true, write:false, create:false, sync:true}; break;
case 'rs+': info = {read:true, write:true, create:false, sync:true}; break;
case 'w': info = {read:false, write:true, create:true, truncate:true}; break;
case 'wx': info = {read:false, write:true, create:true, exclusive:true}; break;
case 'w+': info = {read:true, write:true, create:true, truncate:true}; break;
case 'wx+': info = {read:true, write:true, create:true, exclusive:true}; break;
case 'a': info = {read:false, write:true, create:true, append:true}; break;
case 'ax': info = {read:false, write:true, create:true, append:true, exclusive:true}; break;
case 'a+': info = {read:true, write:true, create:true, append:true}; break;
case 'ax+': info = {read:true, write:true, create:true, append:true, exclusive:true}; break;
default: throw Error("Uknown mode: "+flags); // TODO: throw as `S.err.INVAL`
}
if (info.sync) throw Error("Mode not implemented."); // TODO: what would this require of us?
if (_dir) info._openDir = true;
return info;
};
// TODO: these are great candidates for special test coverage!
var _snInvalid = /[^A-Z0-9$%'-_@~`!(){}^#&.]/g; // NOTE: '.' is not valid but we split it away
exports.shortname = function (name) {
var lossy = false;
// TODO: support preservation of case for otherwise non-lossy name!
name = name.toUpperCase().replace(/ /g, '').replace(/^\.+/, '');
name = name.replace(_snInvalid, function () {
lossy = true;
return '_';
});
var parts = name.split('.'),
basis3 = parts.pop(),
basis8 = parts.join('');
if (!parts.length) {
basis8 = basis3;
basis3 = ' ';
}
if (basis8.length > 8) {
basis8 = basis8.slice(0,8);
// NOTE: technically, spec's "lossy conversion" flag is NOT set by excess length.
// But since lossy conversion and truncated names both need a numeric tail…
lossy = true;
} else while (basis8.length < 8) basis8 += ' ';
if (basis3.length > 3) {
basis3 = basis3.slice(0,3);
lossy = true;
} else while (basis3.length < 3) basis3 += ' ';
return {filename:basis8, extension:basis3, _lossy:lossy};
return {basis:[basis8,basis3], lossy:lossy};
};
//shortname("autoexec.bat") => {basis:['AUTOEXEC','BAT'],lossy:false}
//shortname("autoexecutable.batch") => {basis:['AUTOEXEC','BAT'],lossy:true}
// TODO: OS X stores `shortname("._.Trashes")` as ['~1', 'TRA'] — should we?
var _lnInvalid = /[^a-zA-Z0-9$%'-_@~`!(){}^#&.+,;=[\] ]/g;
exports.longname = function (name) {
name = name.trim().replace(/\.+$/, '').replace(_lnInvalid, function (c) {
if (c.length > 1) throw Error("Internal problem: unexpected match length!");
if (c.charCodeAt(0) > 127) return c;
else throw Error("Invalid character "+JSON.stringify(c)+" in name.");
lossy = true;
return '_';
});
if (name.length > 255) throw Error("Name is too long.");
return name;
};
function nameChkSum(sum, c) {
return ((sum & 1) ? 0x80 : 0) + (sum >>> 1) + c & 0xFF;
}
// WORKAROUND: https://github.com/tessel/beta/issues/335
function reduceBuffer(buf, start, end, fn, res) {
// NOTE: does not handle missing `res` like Array.prototype.reduce would
for (var i = start; i < end; ++i) {
res = fn(res, buf[i]);
}
return res;
}
exports.checksumName = function (buf,off) {
off || (off = 0);
var len = S.dirEntry.fields['Name'].size;
return reduceBuffer(buf, off, off+len, nameChkSum, 0);
};
/* comparing C rounding trick from FAT spec with Math.ceil
function tryBoth(d) {
var a = ((D.RootEntCnt * 32) + (D.BytsPerSec - 1)) / D.BytsPerSec >>> 0,
b = Math.ceil((D.RootEntCnt * 32) / D.BytsPerSec);
if (b !== a) console.log("try", b, a, (b === a) ? '' : '*');
return (b === a);
}
// BytsPerSec — "may take on only the following values: 512, 1024, 2048 or 4096"
[512, 1024, 2048, 4096].forEach(function (bps) {
// RootEntCnt — "a count that when multiplied by 32 results in an even multiple of BPB_BytsPerSec"
for (var evenMultiplier = 0; evenMultiplier < 1024*1024*16; evenMultiplier += 2) {
var rec = (bps * evenMultiplier) / 32;
tryBoth({RootEntCnt:rec, BytsPerSec:bps});
}
});
*/
exports.fmtHex = function (n, ff) {
return (1+ff+n).toString(16).slice(1);
};
exports.delayedCall = function (fn) {
if (!fn) throw Error("No function provided!"); // debug aid
var ctx = this,
args = Array.prototype.slice.call(arguments, 1);
setImmediate(function () {
fn.apply(ctx, args);
});
};
exports.adjustedPos = function (vol, pos, bytes) {
var _pos = {
chain: pos.chain,
sector: pos.sector,
offset: pos.offset + bytes
}, secSize = vol._sectorSize;
while (_pos.offset >= secSize) {
_pos.sector += 1;
_pos.offset -= secSize;
}
return _pos;
};
exports.extend = _xok;
var _prevDbg = Date.now(),
_thresh = 50;
function log(level) {
if (level < log.level) return;
var now = Date.now(),
diff = now - _prevDbg;
arguments[0] = ((diff < _thresh) ? " " : '') + diff.toFixed(0) + "ms";
console.log.apply(console, arguments);
_prevDbg = now;
}
log.DBG = -4;
log.INFO = -3;
log.WARN = -2;
log.ERR = -1;
log.level = log.WARN;
exports.log = log;

View File

@@ -1,25 +0,0 @@
var fs = require('fs');
exports.createDriverSync = function (path, opts) {
opts || (opts = {});
var secSize = 512,
ro = opts.readOnly || false,
fd = fs.openSync(path, (ro) ? 'r' : 'r+'),
s = fs.fstatSync(fd);
return {
sectorSize: secSize,
numSectors: s.size / secSize,
readSectors: function (i, dest, cb) {
fs.read(fd, dest, 0, dest.length, i*secSize, function (e,n,d) {
cb(e,d);
});
},
writeSectors: (ro) ? null : function (i, data, cb) {
fs.write(fd, data, 0, data.length, i*secSize, function (e) {
cb(e);
});
}
};
};

View File

@@ -1,771 +0,0 @@
var events = require('events'),
streams = require('stream'),
fifolock = require('fifolock'),
S = require('./structs.js'),
_ = require('./helpers.js')
//_.log.level = _.log.DBG;
exports.createFileSystem = function (volume, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = null
}
opts = _.extend(
{
// c.f. https://www.kernel.org/doc/Documentation/filesystems/vfat.txt
ro: false,
noatime: true,
modmode: 0111, // or `07000`
umask: 'umask' in process ? process.umask() : 0022,
uid: 'getuid' in process ? process.getuid() : 0,
gid: 'getgid' in process ? process.getgid() : 0,
},
opts
)
if (!volume.writeSectors) opts.ro = true
if (opts.ro) opts.noatime = true // natch
var fs = new events.EventEmitter(),
vol = null,
dir = require('./dir.js'),
c = require('./chains.js'),
q = fifolock()
var GROUP =
_.log.level < _.log.INFO
? q.TRANSACTION_WRAPPER.bind({
postAcquire: function (proceed) {
_.log(_.log.DBG, '=== Starting GROUP ===')
proceed()
},
preRelease: function (finish) {
_.log(_.log.DBG, '=== Finishing GROUP ===')
finish()
},
})
: q.TRANSACTION_WRAPPER
q.acquire(function (unlock) {
// because of this, callers can start before 'ready'
var d = _.allocBuffer(volume.sectorSize)
volume.readSectors(0, d, function (e) {
if (e) fs.emit('error', e)
else {
try {
init(d)
} catch (e) {
fs.emit('error', e)
unlock()
return
}
fs.emit('ready')
}
unlock()
})
})
if (cb) fs.on('error', cb).on('ready', cb.bind(null, null))
function init(bootSector) {
vol = require('./vol.js').init(volume, opts, bootSector)
fs._dirIterator = dir.iterator.bind(dir)
var entryInfoByPath = {},
baseEntry = {
_refs: 0,
_record: function () {
entryInfoByPath[this.path] = this
if (this.parent) this.parent.retain()
return this
},
retain: function () {
if (!this._refs) this._record()
this._refs += 1
return this
},
release: function () {
this._refs -= 1
if (!this._refs) this._rescind()
},
_rescind: function () {
if (this.parent) this.parent.release()
delete entryInfoByPath[this.path]
},
}
fs._createSharedEntry = function (path, entry, chain, parent) {
return _.extend(Object.create(baseEntry), {
_refs: 0, // WORKAROUND: https://github.com/tessel/beta/issues/455
path: path,
entry: entry,
chain: chain,
parent: parent,
}).retain()
}
fs._createSharedEntry('/', { Attr: { directory: true } }, vol.rootDirectoryChain)
fs._sharedEntryForSteps = function (steps, opts, cb) {
// NOTE: may `cb` before returning!
var path = steps.join('/') || '/',
name = steps.pop(), // n.b.
info = entryInfoByPath[path]
if (info) cb(null, info.retain())
else
fs._sharedEntryForSteps(steps, {}, function (e, parentInfo) {
// n.b. `steps` don't include `name`
if (e) cb(e)
else if (!parentInfo.entry.Attr.directory) cb(S.err.NOTDIR())
else
dir.findInDirectory(vol, parentInfo.chain, name, opts, function (e, entry) {
if (e && !opts.prepareForCreate) cb(e)
else if (e) cb(e, { missingChild: _.extend(entry, { name: name }), parent: parentInfo })
else cb(null, fs._createSharedEntry(path, entry, vol.chainForCluster(entry._firstCluster), parentInfo))
})
})
}
fs._updateEntry = dir.updateEntry.bind(dir, vol)
fs._makeStat = dir.makeStat.bind(dir, vol)
fs._addFile = dir.addFile.bind(dir, vol)
fs._initDir = dir.init.bind(dir, vol)
}
/**** ---- CORE API ---- ****/
// NOTE: we really don't share namespace, but avoid first three anyway…
var fileDescriptors = [null, null, null]
fs.open = function (path, flags, mode, cb, _n_) {
if (typeof mode === 'function') {
_n_ = cb
cb = mode
mode = 0666
}
cb = GROUP(
cb,
function () {
var _fd = { flags: null, entry: null, chain: null, pos: 0 },
f = _.parseFlags(flags)
if (vol.opts.ro && (f.write || f.create || f.truncate)) return _.delayedCall(cb, S.err.ROFS())
else _fd.flags = f
fs._sharedEntryForSteps(_.absoluteSteps(path), { prepareForCreate: f.create }, function (e, info) {
if (e && !(e.code === 'NOENT' && f.create && info)) cb(e)
else if (e)
fs._addFile(info.parent.chain, info.missingChild, { dir: f._openDir }, function (e, newEntry, newChain) {
if (e) cb(e)
else finish(fs._createSharedEntry(_.absolutePath(path), newEntry, newChain, info.parent))
})
else if (info && f.exclusive) cb(S.err.EXIST())
else if (info.entry.Attr.directory && !f._openDir) cb(S.err.ISDIR())
else if (f.write && info.entry.Attr.readonly) cb(S.err.ACCES())
else finish(info)
function finish(fileInfo) {
var fd = fileDescriptors.push(_fd) - 1
_fd.info = fileInfo
_fd.entry = fileInfo.entry
_fd.chain = fileInfo.chain
if (f.append) _fd.pos = _fd.entry._size
if (f._openDir) _fd.chain.cacheAdvice = 'WILLNEED'
if (f.truncate && _fd.entry._size)
fs.ftruncate(
fd,
0,
function (e) {
cb(e, fd)
},
'_nested_'
)
else _.delayedCall(cb, null, fd) // (delay in case fs._sharedEntryForSteps all cached!)
}
})
},
_n_ === '_nested_'
)
}
fs.fstat = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb, null, fs._makeStat(_fd.entry))
},
_n_ === '_nested_'
)
}
fs.futimes = function (fd, atime, mtime, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
// NOTE: ctime would get touched on POSIX; but we map that to create time!
else fs._updateEntry(_fd.entry, { atime: atime || true, mtime: mtime || true }, cb)
},
_n_ === '_nested_'
)
}
fs.fchmod = function (fd, mode, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else {
mode &= S._I._chmoddable
if (_fd.entry.Attr.directory) mode |= S._I.FDIR
else if (!_fd.entry.Attr.volume_id) mode |= S._I.FREG
fs._updateEntry(_fd.entry, { mode: mode }, cb)
}
},
_n_ === '_nested_'
)
}
fs.read = function (fd, buf, off, len, pos, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) return _.delayedCall(cb, S.err.BADF())
var _pos = pos === null ? _fd.pos : pos,
_len = Math.min(len, _fd.entry._size - _pos),
_buf = buf.slice(off, off + _len)
_fd.chain.readFromPosition(_pos, _buf, function (e, bytes, slice) {
if (_.workaroundTessel380) _buf.copy(buf, off) // WORKAROUND: https://github.com/tessel/beta/issues/380
_fd.pos = _pos + bytes
if (e || vol.opts.noatime) finish(e)
else fs._updateEntry(_fd.entry, { atime: true }, finish)
function finish(e) {
cb(e, bytes, buf)
}
})
},
_n_ === '_nested_'
)
}
fs._readdir = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) return _.delayedCall(cb, S.err.BADF())
var entryNames = [],
getNextEntry = fs._dirIterator(_fd.chain)
function processNext() {
getNextEntry(function (e, d) {
if (e) cb(e)
else if (!d && !entryNames.length)
cb(null, entryNames) // WORKAROUND: https://github.com/tessel/beta/issues/435
else if (!d)
cb(null, entryNames.sort()) // NOTE: sort not required, but… [simplifies tests for starters!]
else {
if (d._name !== '.' && d._name !== '..') entryNames.push(d._name)
processNext()
}
})
}
processNext()
},
_n_ === '_nested_'
)
}
fs._mkdir = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else fs._initDir(_fd.info, cb)
},
_n_ === '_nested_'
)
}
fs.write = function (fd, buf, off, len, pos, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) return _.delayedCall(cb, S.err.BADF())
var _pos = pos === null || _fd.flags.append ? _fd.pos : pos,
_buf = buf.slice(off, off + len)
if (_pos > _fd.entry._size) {
// TODO: handle huge jumps by zeroing clusters individually?
var padLen = _pos - _fd.entry._size,
padBuf = _.allocBuffer(padLen + _buf.length)
padBuf.fill(0x00, 0, padLen)
_buf.copy(padBuf, padLen)
_pos = _fd.entry._size
_buf = padBuf
}
_fd.chain.writeToPosition(_pos, _buf, function (e) {
_fd.pos = _pos + len
var newSize = Math.max(_fd.entry._size, _fd.pos),
newInfo = { size: newSize, _touch: true }
fs._updateEntry(_fd.entry, newInfo, function (ee) {
cb(e || ee, len, buf)
})
})
},
_n_ === '_nested_'
)
}
fs.ftruncate = function (fd, len, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) return _.delayedCall(cb, S.err.BADF())
var newStats = { size: len, _touch: true }
// NOTE: we order operations for best state in case of only partial success
if (len === _fd.entry._size) _.delayedCall(cb)
else if (len < _fd.entry._size)
fs._updateEntry(_fd.entry, newStats, function (e) {
if (e) cb(e)
else _fd.chain.truncate(Math.ceil(len / _fd.chain.sectorSize), cb)
})
// TODO: handle huge file expansions without as much memory pressure
else
_fd.chain.writeToPosition(_fd.entry._size, _.allocBuffer(len - _fd.entry._size, 0x00), function (e) {
if (e) cb(e)
else fs._updateEntry(_fd.entry, newStats, cb)
})
},
_n_ === '_nested_'
)
}
// 'NORMAL', 'SEQUENTIAL', 'RANDOM', 'WILLNEED', 'DONTNEED', 'NOREUSE'
fs._fadviseSync = function (fd, off, len, advice) {
if (off !== 0 || len !== 0) throw Error('Cache advise can currently be given only for whole file!')
var _fd = fileDescriptors[fd]
if (!_fd) throw S.err.BADF()
else _fd.chain.cacheAdvice = advice
}
fs.fsync = function (fd, cb) {
// NOTE: we'll need to flush write cache here once we have one…
var _fd = fileDescriptors[fd]
if (!_fd) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb)
}
fs.close = function (fd, cb) {
var _fd = fileDescriptors[fd]
if (!_fd) _.delayedCall(cb, S.err.BADF())
else setTimeout(_fd.info.release.bind(_fd.info), 500), _.delayedCall(cb, (fileDescriptors[fd] = null))
}
/* STREAM WRAPPERS */
var workaroundTessel436
try {
new require('stream').Readable({ encoding: 'utf8' })
new streams.Readable({ encoding: 'utf8' })
} catch (e) {
workaroundTessel436 = true
}
function _createStream(StreamType, path, opts) {
// [NOT REALLY A] WORKAROUND: https://github.com/tessel/beta/issues/436
if (workaroundTessel436 && 'encoding' in opts) {
console.warn('Tessel does not currently support encoding option for Readable streams, discarding!')
delete opts.encoding
}
var fd = opts.fd !== null ? opts.fd : '_opening_',
pos = opts.start,
stream = new StreamType(opts)
if (fd === '_opening_')
fs.open(path, opts.flags, opts.mode, function (e, _fd) {
if (e) {
fd = '_open_error_'
stream.emit('error', e)
} else {
fd = _fd
fs._fadviseSync(fd, 0, 0, 'SEQUENTIAL')
stream.emit('open', fd)
}
})
function autoClose(tombstone) {
// NOTE: assumes caller will clear `fd`
if (opts.autoClose)
fs.close(fd, function (e) {
if (e) stream.emit('error', e)
else stream.emit('close')
})
fd = tombstone
}
if (StreamType === streams.Readable) {
stream._read = function (n) {
var buf
// TODO: optimize to fetch at least a full sector regardless of `n`…
n = Math.min(n, opts.end - pos)
if (fd === '_opening_')
stream.once('open', function () {
stream._read(n)
})
else if (pos > opts.end) stream.push(null)
else if (n > 0)
(buf = _.allocBuffer(n)),
fs.read(fd, buf, 0, n, pos, function (e, n, d) {
if (e) {
autoClose('_read_error_')
stream.emit('error', e)
} else stream.push(n ? d.slice(0, n) : null)
}),
(pos += n)
else stream.push(null)
}
stream.once('end', function () {
autoClose('_ended_')
})
} else if (StreamType === streams.Writable) {
stream.bytesWritten = 0
stream._write = function (data, _enc, cb) {
if (fd === '_opening_')
stream.once('open', function () {
stream._write(data, null, cb)
})
else
fs.write(fd, data, 0, data.length, pos, function (e, n) {
if (e) {
autoClose('_write_error_')
cb(e)
} else {
stream.bytesWritten += n
cb()
}
}),
(pos += data.length)
}
stream.once('finish', function () {
autoClose('_finished_')
})
}
return stream
}
fs.createReadStream = function (path, opts) {
return _createStream(
streams.Readable,
path,
_.extend(
{
start: 0,
end: Infinity,
flags: 'r',
mode: 0666,
encoding: null,
fd: null, // ??? see https://github.com/joyent/node/issues/7708
autoClose: true,
},
opts
)
)
}
fs.createWriteStream = function (path, opts) {
return _createStream(
streams.Writable,
path,
_.extend(
{
start: 0,
flags: 'w',
mode: 0666,
//encoding: null, // see https://github.com/joyent/node/issues/7710
fd: null, // ??? see https://github.com/joyent/node/issues/7708
autoClose: true,
},
opts,
{ decodeStrings: true, objectMode: false }
)
)
}
/* PATH WRAPPERS (albeit the only public interface for some folder operations) */
function _fdOperation(path, opts, fn, cb) {
cb = GROUP(cb, function () {
opts.advice || (opts.advice = 'NORMAL')
fs.open(
path,
opts.flag,
function (e, fd) {
if (e) cb(e)
else
fs._fadviseSync(fd, 0, 0, opts.advice),
fn(fd, function () {
var ctx = this,
args = arguments
fs.close(
fd,
function (closeErr) {
cb.apply(ctx, args)
},
'_nested_'
)
})
},
'_nested_'
)
})
}
fs.stat = fs.lstat = function (path, cb) {
_fdOperation(
path,
{ flag: 'r' },
function (fd, cb) {
fs.fstat(fd, cb, '_nested_')
},
cb
)
}
fs.exists = function (path, cb) {
fs.stat(path, function (err) {
cb(err ? false : true)
})
}
fs.readFile = function (path, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'r')
opts.advice || (opts.advice = 'NOREUSE')
_fdOperation(
path,
opts,
function (fd, cb) {
fs.fstat(
fd,
function (e, stat) {
if (e) return cb(e)
else {
var buffer = _.allocBuffer(stat.size)
fs.read(
fd,
buffer,
0,
buffer.length,
null,
function (e) {
if (e) cb(e)
else cb(null, opts.encoding ? buffer.toString(opts.encoding) : buffer)
},
'_nested_'
)
}
},
'_nested_'
)
},
cb
)
}
fs.writeFile = function (path, data, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'w')
opts.advice || (opts.advice = 'NOREUSE')
_fdOperation(
path,
opts,
function (fd, cb) {
if (typeof data === 'string') data = _.bufferFrom(data, opts.encoding || 'utf8')
fs.write(
fd,
data,
0,
data.length,
null,
function (e) {
cb(e)
},
'_nested_'
)
},
cb
)
}
fs.appendFile = function (path, data, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'a')
fs.writeFile(path, data, opts, cb)
}
fs.truncate = function (path, len, cb) {
_fdOperation(
path,
{ flag: 'r+' },
function (fd, cb) {
fs.ftruncate(fd, len, cb, '_nested_')
},
cb
)
}
fs.readdir = function (path, cb) {
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
fs._readdir(fd, cb, '_nested_')
},
cb
)
}
fs.mkdir = function (path, mode, cb) {
if (typeof mode === 'function') {
cb = mode
mode = 0777
}
_fdOperation(
path,
{ flag: '\\wx' },
function (fd, cb) {
fs._mkdir(fd, cb, '_nested_')
},
cb
)
}
fs.utimes = function (path, atime, mtime, cb) {
_fdOperation(
path,
{ flag: 'r+' },
function (fd, cb) {
fs.futimes(fd, atime, mtime, cb, '_nested_')
},
cb
)
}
fs.chmod = fs.lchmod = function (path, mode, cb) {
_fdOperation(
path,
{ flag: '\\r+' },
function (fd, cb) {
fs.fchmod(fd, mode, cb, '_nested_')
},
cb
)
}
fs.chown = fs.lchown = function (path, uid, gid, cb) {
_fdOperation(
path,
{ flag: '\\r+' },
function (fd, cb) {
fs.fchown(fd, uid, gid, cb, '_nested_')
},
cb
)
}
/* STUBS */
fs.link = function (src, dst, cb) {
// NOTE: theoretically we _could_ do hard links [with untracked `stat.nlink` count…]
_.delayedCall(cb, S.err.NOSYS())
}
fs.symlink = function (src, dst, type, cb) {
if (typeof type === 'function') {
cb = type
type = null
}
_.delayedCall(cb, S.err.NOSYS())
}
fs.readlink = function (path, cb) {
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
// the named file is *never* a symbolic link…
// NOTE: we still use _fdOperation for catching e.g. NOENT/NOTDIR errors…
cb(S.err.INVAL())
},
cb
)
}
fs.realpath = function (path, cache, cb) {
if (typeof cache === 'function') {
cb = cache
cache = null
}
if (cache)
_.delayedCall(cb, S.err.NOSYS()) // TODO: what would be involved here?
else
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
cb(null, _.absolutePath(path))
},
cb
)
}
fs.fchown = function (fd, uid, gid, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb, S.err.NOSYS())
},
_n_ === '_nested_'
)
}
// See https://github.com/natevw/fatfs/pull/31/files
fs.createLabel = function (name, cb) {
fs.open(name, 'a', (e, _fd) => {
if (e) {
cb(e)
} else {
let fd = fileDescriptors[_fd]
fd.entry.Attr.volume_id = true
fd.entry.FstClusLO = 0
fd.entry.FstClusHI = 0
fs._updateEntry(fd.entry, {}, cb)
}
})
}
return fs
}

View File

@@ -1,36 +0,0 @@
#! /bin/sh
# NOTE: this only works on OS X
# TODO: Linux could use mkfs stuff without attach?
# ./make_sample.sh /tmp/fat16.img FAT16
FILE=$1
: ${FILE:?must be provided as first argument}
TYPE=$2
: ${TYPE:?must be provided as second argument}
SIZE=$3 # in MB
case "$TYPE" in
"FAT12") : ${SIZE:=1} # <128
;;
"FAT16") : ${SIZE:=5} # >4
;;
"FAT32") : ${SIZE:=33} # >32
;;
*) : ${SIZE:=1} # e.g. ExFAT
esac
echo Making $TYPE of $SIZE MB at $FILE
if [[ "$TYPE" = FAT* ]]
then
TYPE="MS-DOS $TYPE"
fi
dd if=/dev/zero of=$FILE bs=1048576 count=$SIZE
DEV=`hdiutil attach $FILE -nomount`
diskutil eraseVolume "$TYPE" "FATFS TEST" $DEV
hdiutil detach $DEV

View File

@@ -1,32 +0,0 @@
{
"private": false,
"name": "@vates/fatfs",
"version": "0.11.0",
"description": "fs implementation on top of raw FAT16/FAT32 block source",
"main": "index.js",
"scripts": {
"postversion": "npm publish --access public"
},
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra"
},
"keywords": [
"filesystem",
"fs",
"fat",
"fat32",
"fat16"
],
"author": "Vates SAS",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/vatesfr/xen-orchestra/issues"
},
"homepage": "https://github.com/vatesfr/xen-orchestra",
"dependencies": {
"fifolock": "^1.0.0",
"struct-fu": "^1.2.1",
"xok": "^1.0.0"
}
}

View File

@@ -1,255 +0,0 @@
// see http://staff.washington.edu/dittrich/misc/fatgen103.pdf
// and http://www.cse.scu.edu/~tschwarz/COEN252_09/Lectures/FAT.html
var _ = require('struct-fu'),
__ = require("./helpers.js");
var bootBase = _.struct([
_.byte('jmpBoot', 3),
_.char('OEMName', 8),
_.uint16le('BytsPerSec'),
_.uint8('SecPerClus'),
_.uint16le('ResvdSecCnt'), // Rsvd in table, but Resvd in calcs…
_.uint8('NumFATs'),
_.uint16le('RootEntCnt'),
_.uint16le('TotSec16'),
_.uint8('Media'),
_.uint16le('FATSz16'),
_.uint16le('SecPerTrk'),
_.uint16le('NumHeads'),
_.uint32le('HiddSec'),
_.uint32le('TotSec32')
]);
var bootInfo = _.struct([
_.uint8('DrvNum'),
_.uint8('Reserved1'),
_.uint8('BootSig'),
_.uint32le('VolID'),
_.char('VolLab', 11),
_.char('FilSysType', 8)
]);
exports.boot16 = _.struct([
bootBase,
bootInfo
]);
exports.boot32 = _.struct([
bootBase,
_.uint32le('FATSz32'),
_.struct('ExtFlags', [
_.ubit('NumActiveFAT', 4),
_.ubit('_reserved1', 3),
_.bool('MirroredFAT'),
_.ubit('_reserved2', 8)
].reverse()),
_.struct('FSVer', [
_.uint8('Major'),
_.uint8('Minor')
]),
_.uint32le('RootClus'),
_.uint16le('FSInfo'),
_.uint16le('BkBootSec'),
_.byte('Reserved', 12),
bootInfo
]);
var _time = _.struct([
_.ubit('hours',5),
_.ubit('minutes',6),
_.ubit('seconds_2',5)
]), time = {
valueFromBytes: function (buf, off) {
off || (off = {bytes:0});
var _buf = __.bufferFrom([buf[off.bytes+1], buf[off.bytes+0]]),
val = _time.valueFromBytes(_buf);
off.bytes += this.size;
return val;
},
bytesFromValue: function (val, buf, off) {
val || (val = {hours:0, minutes:0, seconds_2:0});
buf || (buf = __.allocBuffer(this.size));
off || (off = {bytes:0});
var _buf = _time.bytesFromValue(val);
buf[off.bytes+1] = _buf[0];
buf[off.bytes+0] = _buf[1];
off.bytes += this.size;
return buf;
},
size: _time.size
};
var _date = _.struct([
_.ubit('year',7),
_.ubit('month',4),
_.ubit('day',5)
]), date = {
valueFromBytes: function (buf, off) {
off || (off = {bytes:0});
var _buf = __.bufferFrom([buf[off.bytes+1], buf[off.bytes+0]]),
val = _date.valueFromBytes(_buf);
off.bytes += this.size;
return val;
},
bytesFromValue: function (val, buf, off) {
val || (val = {year:0, month:0, day:0});
buf || (buf = __.allocBuffer(this.size));
off || (off = {bytes:0});
var _buf = _date.bytesFromValue(val);
buf[off.bytes+1] = _buf[0];
buf[off.bytes+0] = _buf[1];
off.bytes += this.size;
return buf;
},
size: _date.size
};
exports.dirEntry = _.struct([
_.struct('Name', [
_.char('filename',8),
_.char('extension',3)
]),
_.struct('Attr', [
_.bool('readonly'),
_.bool('hidden'),
_.bool('system'),
_.bool('volume_id'),
_.bool('directory'),
_.bool('archive'),
_.ubit('reserved', 2)
].reverse()),
_.byte('NTRes', 1),
_.uint8('CrtTimeTenth'),
_.struct('CrtTime', [time]),
_.struct('CrtDate', [date]),
_.struct('LstAccDate', [date]),
_.uint16le('FstClusHI'),
_.struct('WrtTime', [time]),
_.struct('WrtDate', [date]),
_.uint16le('FstClusLO'),
_.uint32le('FileSize')
]);
exports.entryDoneFlag = 0x00;
exports.entryFreeFlag = 0xE5;
exports.entryIsE5Flag = 0x05;
exports.dirEntry_simple = _.struct([
_.struct('Name', [
_.char('filename',8),
_.char('extension',3)
]),
_.padTo(exports.dirEntry.size)
/*
_.uint8('Attr_raw'),
_.byte('NTRes', 1),
_.byte('Crt_raw', 1+2+2),
_.byte('Lst_raw', 2),
_.uint16le('FstClusHI'),
_.byte('Wrt_raw', 2+2),
_.uint16le('FstClusLO'),
_.uint32le('FileSize')
*/
]);
exports.lastLongFlag = 0x40;
exports.longDirFlag = 0x0F;
exports.longDirEntry = _.struct([
_.uint8('Ord'),
_.char16le('Name1', 10),
_.uint8('Attr_raw'),
_.uint8('Type'),
_.uint8('Chksum'),
_.char16le('Name2', 12),
_.uint16le('FstClusLO'),
_.char16le('Name3', 4)
]);
if (exports.longDirEntry.size !== exports.dirEntry.size) throw Error("Structs ain't right!");
exports.fatField = {
'fat12': _.struct('Status', [
_.ubit('field0bc', 8),
_.ubit('field1c', 4),
_.ubit('field0a', 4),
_.ubit('field1ab', 8),
]),
'fat16': _.uint16le('Status'),
'fat32': _.uint32le('Status') // more properly this 4 bits reserved + uint28le
};
exports.fatPrefix = {
'fat12': 0xF00,
'fat16': 0xFF00,
'fat32': 0x0FFFFF00
};
exports.fatStat = {
free: 0x00,
_undef: 0x01,
rsvMin: 0xF0,
bad: 0xF7,
eofMin: 0xF8,
eof: 0xFF
};
exports._I = {
RUSR: 0400,
WUSR: 0200,
XUSR: 0100,
RGRP: 0040,
WGRP: 0020,
XGRP: 0010,
ROTH: 0004,
WOTH: 0002,
XOTH: 0001,
SUID: 04000,
SGID: 02000,
SVTX: 01000,
FDIR: 040000,
FREG: 0100000,
};
exports._I.RWXU = exports._I.RUSR | exports._I.WUSR | exports._I.XUSR;
exports._I.RWXG = exports._I.RGRP | exports._I.WGRP | exports._I.XGRP;
exports._I.RWXO = exports._I.ROTH | exports._I.WOTH | exports._I.XOTH;
exports._I._sss = exports._I.SUID | exports._I.SGID | exports._I.SVTX;
exports._I._chmoddable = exports._I.RWXU | exports._I.RWXG | exports._I.RWXO | exports._I._sss;
var _errors = {
IO: "Input/output error",
NOENT: "No such file or directory",
INVAL: "Invalid argument",
EXIST: "File exists",
NAMETOOLONG: "Filename too long",
NOSPC: "No space left on device",
NOSYS: "Function not supported",
ROFS: "ROFLCopter file system",
NOTDIR: "Not a directory",
BADF: "Bad file descriptor",
EXIST: "File exists",
ISDIR: "Is a directory",
ACCES: "Permission denied",
NOSYS: "Function not implemented",
_TODO: "Not implemented yet!"
};
exports.err = {};
Object.keys(_errors).forEach(function (sym) {
var msg = _errors[sym];
exports.err[sym] = function () {
var e = new Error(msg);
e.code = sym;
return e;
};
});

View File

@@ -1,338 +0,0 @@
// can be called from CLI with image type, absolute path to an image, or, required as a module
var _ = require("./helpers.js");
var type = process.argv[2];
if (module.parent) exports.startTests = startTests;
else if (!type) throw "Usage: node test [FAT12|FAT16|FAT32|ExFAT|…|/path/to/image]";
else if (type[0] === '/') testWithImage(type);
else {
var uniq = Math.random().toString(36).slice(2),
IMG = require('os').tmpdir()+"fatfs-test-"+uniq+".img";
require('child_process').exec("./make_sample.sh "+JSON.stringify(IMG)+" "+JSON.stringify(type), function (e,out,err) {
if (e) throw e;
console.warn(err.toString());
//console.log(out.toString());
testWithImage(IMG);
//console.log("open", IMG);
//return;
require('fs').unlink(IMG, function (e) {
if (e) console.warn("Error cleaning up test image", e);
});
});
}
function testWithImage(imagePath) {
var vol = require("./img_volume.js").createDriverSync(imagePath);
startTests(vol);
}
function startTests(vol, waitTime) {
var fatfs = require("./"),
fs = fatfs.createFileSystem(vol, {umask:0020, uid:99, gid:42});
waitTime || (waitTime = 0.5e3);
[
'mkdir','readdir',
//'rename','unlink','rmdir',
'close','open','fsync',
'ftruncate','truncate',
'write','read','readFile','writeFile', 'appendFile',
'chown','lchown','fchown',
'chmod','lchmod', 'fchmod',
'utimes','futimes',
'stat','lstat','fstat','exists',
'link','symlink','readlink','realpath',
//'watchFile','unwatchFile','watch'
].forEach(function (method) { assert(method in fs, "fs."+method+" has implementation."); });
var BASE_DIR = "/fat_test-"+Math.random().toFixed(20).slice(2),
FILENAME = "Simple File.txt",
TEXTDATA = "Hello world!";
var isReady = false;
fs.on('ready', function () {
assert(isReady = true, "Driver is ready.");
}).on('error', function (e) {
assert(e, "If fs driver fires 'error' event, it should include error object.");
assert(false, "…but driver should not error when initializing in our case.");
});
setTimeout(function () {
assert(isReady, "Driver fired ready event in timely fashion.");
}, waitTime);
fs.readdir("/", function (e,files) {
assert(isReady, "Method completed after 'ready' event.");
assert(!e, "No error reading root directory.");
assert(Array.isArray(files), "Got a list of files: "+files);
});
fs.mkdir(BASE_DIR, function (e) {
assert(!e, "No error from fs.mkdir");
fs.readdir(BASE_DIR, function (e,arr) {
assert(!e, "No error from fs.readdir");
assert(arr.length === 0 , "No files in BASE_DIR yet.");
});
var file = [BASE_DIR,FILENAME].join('/');
fs.writeFile(file, TEXTDATA, function (e) {
assert(!e, "No error from fs.writeFile");
startStreamTests();
fs.realpath(file, function (e,path) {
assert(!e, "No error from basic fs.realpath call.");
assert(path === file, "We already had the real path.");
});
fs.realpath([BASE_DIR,".","garbage",".","..",FILENAME].join('/'), function (e,path) {
assert(!e, "No error from fluffy fs.realpath call.");
assert(path === file, "Fixed fluffy path matches normal one.");
});
fs.realpath([BASE_DIR,"non","existent","path"].join('/'), function (e) {
assert(e, "Expected error calling fs.realpath on non-existent file.");
});
fs.readdir(BASE_DIR, function (e, arr) {
assert(!e, "Still no error from fs.readdir");
assert(arr.length === 2, "Test directory contains two files."); // (ours + startStreamTests's)
assert(arr[0] === FILENAME, "Filename is correct.");
fs.stat(file, function (e,d) {
assert(!e, "No error from fs.stat");
assert(d.isFile() === true, "Result is a file…");
assert(d.isDirectory() === false, "…and not a directory.");
assert(d.size === Buffer.byteLength(TEXTDATA), "Size matches length of content written.");
});
fs.exists(file, function (bool) {
assert(bool === true, "File exists.");
});
fs.readFile(file, {encoding:'utf8'}, function (e, d) {
assert(!e, "No error from fs.readFile");
assert(d === TEXTDATA, "Data matches what was written.");
});
// now, overwrite the same file and make sure that goes well too
fs.writeFile(file, _.bufferFrom([0x42]), function (e) {
assert(!e, "Still no error from fs.writeFile");
fs.readdir(BASE_DIR, function (e, arr) {
assert(!e, "No error from fs.readdir");
assert(arr.length === 2, "Test directory still contains two files.");
assert(arr[0] === FILENAME, "Filename still correct.");
fs.stat(file, function (e,d) {
assert(!e, "Still no error from fs.stat");
assert(d.isFile() === true, "Result is still a file…");
assert(d.isDirectory() === false, "…and not a directory.");
assert(d.size === 1, "Size matches length of now-truncated content.");
});
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile");
assert(Buffer.isBuffer(d), "Result without encoding is a buffer.");
assert(d.length === 1, "Buffer is correct size.");
assert(d[0] === 0x42, "Buffer content is correct.");
fs.truncate(file, 1025, function (e) {
assert(!e, "No error from fs.truncate (extending)");
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile after extension");
assert(d.length === 1025, "Read after extension is correct size.");
assert(d[0] === 0x42, "First byte is still correct.");
var allZeroes = true;
for (var i = 1, len = d.length; i < len; ++i) if (d[i] !== 0) allZeroes = false;
assert(allZeroes, "Extended portion of file is zero-filled.");
fs.truncate(file, 3, function (e) {
assert(!e, "No error from fs.truncate (shortening)");
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile after shortening.");
assert(d.length === 3, "Read after shortening is correct size.");
assert(d[0] === 0x42, "First byte is still correct.");
assert(d[1] === 0x00, "Second byte is still correct.");
assert(d[2] === 0x00, "Third byte is still correct.");
proceedWithMoreTests();
});
});
});
});
});
});
});
});
});
function proceedWithMoreTests() {
var fd;
fs.open(file, 'r', function (e, _fd) {
assert(!e, "No error from fs.open.");
fd = _fd;
});
var was = "\u0042\u0000\u0000",
str = "abc";
fs.appendFile(file, str, function (e) {
assert(!e, "No error from fs.appendFile.");
assert(fd, "File descriptor opened before appendFile called.");
var buf = _.allocBuffer(str.length);
fs.read(fd, buf, 0, buf.length, was.length, function (e,n,d) {
assert(!e, "No error from fs.read after append.");
assert(n === str.length, "All appended data was readable.");
assert(d === buf, "Buffer returned from fs.read matched what was passed in.");
assert(d.toString() === str, "Correct data found where expected.");
});
});
fs.readFile(file, {encoding:'ascii'}, function (e,d) {
assert(!e, "No error from fs.appendFile.");
assert(d.length === 6, "Read is correct size after append.");
assert(d === was+str, "Read string matches what was written and then appended.");
fs.open(file, 'a', function (e, fd2) {
assert(!e, "No error from second open of file.");
var str2 = "zyx",
buf2 = _.allocBuffer(str2.length+2);
buf2.write(str2, 1);
fs.write(fd2, buf2, 1, buf2.length-2, was.length, function (e,n,d) {
assert(!e, "No error from appending fs.write.");
assert(n === buf2.length-2, "Wrote proper amount from buffer.");
assert(d === buf2, "Returned original buffer.");
buf2.fill(0);
buf2[0] = 0xFF;
fs.read(fd, buf2, 1, buf2.length-1, was.length, function (e,n,d) {
assert(!e, "No error from twice-appended read.");
assert(n === buf2.length-1, "Read proper amount into buffer.");
assert(buf2[0] === 0xFF, "Read left first byte in buffer properly alone.");
assert(d.slice(1).toString() === (str+str2).slice(0, buf2.length-1), "Data was appended, not written at position.");
});
});
});
});
var F = [BASE_DIR,"Manually inspect from time to time, please!.txt"].join('/'),
S = 512,
N = 16,
b = _.allocBuffer(S*N);
for (var i = 0; i < N; ++i) b.slice(S*i, S*i+S).fill(i.toString(16).charCodeAt(0));
fs.writeFile(F, b, function (e) {
assert(!e, "No error from fs.writeFile with counting blocks.");
fs.readFile(F, function (e,d) {
assert(!e, "No error from fs.readFile with counting blocks.");
assert(d.length === b.length, "Readback is correct size");
var matched = true;
for (var i = 0; i < S*N; ++i) if (b[i] !== d[i]) matched = false;
assert(matched, "Readback matches write byte-for-byte");
});
});
fs.stat(F, function (e,d) {
assert(!e, "No error from fs.stat on counting blocks file.");
assert(d.atime instanceof Date && !isNaN(d.atime.getTime()), "Access time is a valid date.");
assert(d.mtime instanceof Date && !isNaN(d.mtime.getTime()), "Modify time is a valid date.");
assert(d.ctime instanceof Date && !isNaN(d.ctime.getTime()), "Change^WCreate time is a valid date.");
var tf = d.ctime.getTime(),
ct = Date.now();
assert(tf - 2*waitTime < ct && ct < tf + 2*waitTime, "Create time is within ± two `waitTime`s of now.");
fs.utimes(F, new Date(2009, 7-1, 2), null, function (e) {
assert(!e, "No error from fs.utimes.");
fs.stat(F, function (e,d2) {
assert(!e, "No fs.stat error after touching timestamps.");
assert(+d.ctime === +d2.ctime, "Create time not changed by fs.utimes");
assert(d2.atime.toString().indexOf("Jul 02 2009 00:00:00") === 4, "Access time set correctly");
var tf = d2.mtime.getTime(),
ct = Date.now();
assert(tf-2e3 < ct && ct < tf+2e3+waitTime, "Modify time is within ± a few seconds of now.");
// NOTE: due to serialization, this can check results of the `fs.chmod` below, too!
assert(!(d2.mode & 0100), "Archive bit is now unset.");
assert(!(d2.mode & 0222), "Writable perms are unset.");
assert(d2.mode & 0100000, "Regular file bit is set.");
});
});
assert(d.uid === 99, "Desired UID applied.");
assert(d.gid === 42, "Desired GID applied.");
assert(d.mode & 0100, "Archive bit is set.");
assert(d.mode & 0200, "Writable perm is set for user.");
assert((d.mode & 0022) === 0002, "Writable perm is only masked out for group.");
fs.chmod(F, 0422, function (e) {
assert(!e, "No error from fs.chmod.");
});
});
fs.chown(F, 99, 256, function (e) {
assert(e && e.code === 'NOSYS', "Expected error from fs.fchown.");
});
}
function startStreamTests() {
var file2 = [BASE_DIR,FILENAME+"2"].join('/'),
outStream = fs.createWriteStream(file2);
var outStreamOpened = false;
outStream.on('open', function (fd) {
outStreamOpened = true;
assert(typeof fd === 'number', "Got file descriptor on fs.createWriteStream open.");
}).on('error', function (e) {
assert(e, "If fs.createWriteStream fires 'error' event, it should include error object.");
assert(false, "But, fs.createWriteStream should not error during these tests.");
});
setTimeout(function () {
assert(outStreamOpened, "outStream fired 'open' event in a timely fashion.");
}, waitTime);
var TEXT_MOD = TEXTDATA.toLowerCase()+"\n",
NUM_REPS = (waitTime <= 1e3) ? 1024 : 16;
outStream.write(TEXT_MOD, 'utf16le');
outStream.write("Ο καλύτερος χρόνος να φυτευτεί ένα \ud83c\udf31 είναι δέκα έτη πριν.", 'utf16le');
outStream.write("La vez del segundo mejor ahora está.\n", 'utf16le');
for (var i = 0; i < NUM_REPS; ++i) outStream.write("123456789\n", 'ascii');
outStream.write("JavaScript how do they work\n", 'utf16le');
outStream.write("The end, almost.\n", 'utf16le');
outStream.end(TEXTDATA, 'utf16le');
var outStreamFinished = false;
outStream.on('finish', function () {
outStreamFinished = true;
var inStream = fs.createReadStream(file2, {start:NUM_REPS*10, encoding:'utf16le', autoClose:false}),
gotData = false, gotEOF = false, inStreamFD = null;
inStream.on('open', function (fd) {
assert(typeof fd === 'number', "Got file descriptor on fs.createReadStream open.");
inStreamFD = fd;
});
inStream.on('data', function (d) {
gotData = true;
assert(typeof d === 'string', "Data returned as string.");
assert(d.slice(d.length-TEXTDATA.length) === TEXTDATA, "End of file matches what was written.");
});
inStream.on('end', function () {
gotEOF = true;
var len = Buffer.byteLength(TEXT_MOD, 'utf16le'),
buf = _.allocBuffer(len);
fs.fsync(inStreamFD, function (e) {
assert(!e, "No error from proper fsync.");
});
fs.fsync('garbage', function (e) {
assert(e, "Expected error from garbage fsync.");
});
fs.read(inStreamFD, buf, 0, len, 0, function (e,n,d) {
assert(!e, "No error reading from beginning of inStream's file descriptor.");
assert(n === len, "Read complete buffer at beginning of inStream's fd.");
assert(d.toString('utf16le') === TEXT_MOD, "Data matches at beginning of inStream's fd.");
fs.close(inStreamFD, function (e) {
assert(!e, "No error closing inStream's fd.");
});
});
});
setTimeout(function () {
assert(gotData, "inStream fired 'data' event in a timely fashion.");
setTimeout(function () {
assert(gotEOF, "inStream fired 'eof' event in a timely fashion.");
}, waitTime);
}, waitTime);
});
setTimeout(function () {
assert(outStreamFinished, "outStream fired 'finish' event in a timely fashion.");
}, 2*waitTime);
}
});
}
function assert(b,msg) { if (!msg) console.warn("no msg", Error().stack); if (!b) throw Error("Assertion failure. "+msg); else console.log(msg); }

View File

@@ -1,165 +0,0 @@
var S = require("./structs.js"),
c = require("./chains.js"),
$ = require("./cache.js"),
_ = require("./helpers.js");
exports.init = function (volume, opts, bootSector) {
if (bootSector[510] !== 0x55 || bootSector[511] !== 0xAA) throw Error("Invalid volume signature!");
var isFAT16 = bootSector.readUInt16LE(S.boot16.fields['FATSz16'].offset),
bootStruct = (isFAT16) ? S.boot16 : S.boot32,
BS = bootStruct.valueFromBytes(bootSector);
_.log(_.log.DBG, "Boot sector info:", BS);
bootSector = null; // allow GC
if (!BS.BytsPerSec) throw Error("This looks like an ExFAT volume! (unsupported)");
else if (BS.BytsPerSec !== volume.sectorSize) throw Error("Sector size mismatch with FAT table.");
var FATSz = (isFAT16) ? BS.FATSz16 : BS.FATSz32,
rootDirSectors = Math.ceil((BS.RootEntCnt * 32) / BS.BytsPerSec),
firstDataSector = BS.ResvdSecCnt + (BS.NumFATs * FATSz) + rootDirSectors,
totSec = (BS.TotSec16) ? BS.TotSec16 : BS.TotSec32,
dataSec = totSec - firstDataSector,
countofClusters = Math.floor(dataSec / BS.SecPerClus);
// avoid corrupting sectors from other partitions or whatnot
if (totSec > volume.numSectors) throw Error("Volume size mismatch!");
var fatType;
if (countofClusters < 4085) {
fatType = 'fat12';
} else if (countofClusters < 65525) {
fatType = 'fat16';
} else {
fatType = 'fat32';
}
_.log(_.log.DBG, "rootDirSectors", rootDirSectors, "firstDataSector", firstDataSector, "countofClusters", countofClusters, "=>", fatType);
var vol = {};
vol.opts = opts;
vol._sectorSize = BS.BytsPerSec;
vol._sectorsPerCluster = BS.SecPerClus;
vol._firstSectorOfCluster = function (n) {
return firstDataSector + (n-2)*vol._sectorsPerCluster;
};
vol._makeCache = function () {
return $.wrapDriver(volume);
};
vol._readSectors = function (cache, secNum, dest, cb) {
if (typeof dest === 'function') {
cb = dest;
dest = _.allocBuffer(vol._sectorSize);
}
_.log(_.log.DBG, "vol._readSectors", secNum, dest.length);
if (secNum < volume.numSectors) cache.readSectors(secNum, dest, function (e) { cb(e, dest); });
else throw Error("Invalid sector number!");
};
vol._writeSectors = function (cache, secNum, data, cb) {
_.log(_.log.DBG, "vol._writeSectors", secNum, data.length);
// NOTE: these are internal assertions, public API will get proper `S.err`s
if (data.length % volume.sectorSize) throw Error("Buffer length not a multiple of sector size");
else if (opts.ro) throw Error("Read-only filesystem");
else if (secNum < volume.numSectors) cache.writeSectors(secNum, data, cb);
else throw Error("Invalid sector number!");
};
function fatInfoForCluster(n) {
var entryStruct = S.fatField[fatType],
FATOffset = (fatType === 'fat12') ? Math.floor(n/2) * entryStruct.size : n * entryStruct.size,
SecNum = BS.ResvdSecCnt + Math.floor(FATOffset / BS.BytsPerSec);
EntOffset = FATOffset % BS.BytsPerSec;
return {sector:SecNum-BS.ResvdSecCnt, offset:EntOffset, struct:entryStruct};
}
// TODO: all this FAT manipulation is crazy inefficient! needs read caching *and* write caching
// …the best place for cache might be in `volume` handler, though. add a `flush` method to that spec?
// TODO: how should we handle redundant FATs? mirror every write? just ignore completely? copy-on-eject?
var fatChain = c.sectorChain(vol, BS.ResvdSecCnt, FATSz);
fatChain.cacheAdvice = 'RANDOM';
vol.fetchFromFAT = function (clusterNum, cb) {
var info = fatInfoForCluster(clusterNum);
fatChain.readFromPosition(info, info.struct.size, function (e,n,d) {
if (e) return cb(e);
var status = info.struct.valueFromBytes(d), prefix;
if (fatType === 'fat12') {
if (clusterNum % 2) {
status = (status.field1ab << 4) + status.field1c;
} else {
status = (status.field0a << 8) + status.field0bc;
}
}
else if (fatType === 'fat32') {
status &= 0x0FFFFFFF;
}
var prefix = S.fatPrefix[fatType];
if (status === S.fatStat.free) cb(null, 'free');
else if (status === S.fatStat._undef) cb(null, '-invalid-');
else if (status > prefix+S.fatStat.eofMin) cb(null, 'eof');
else if (status === prefix+S.fatStat.bad) cb(null, 'bad');
else if (status > prefix+S.fatStat.rsvMin) cb(null, 'reserved');
else cb(null, status);
});
};
vol.storeToFAT = function (clusterNum, status, cb) {
if (typeof status === 'string') {
status = S.fatStat[status];
status += S.fatPrefix[fatType];
}
var info = fatInfoForCluster(clusterNum);
// TODO: technically fat32 needs to *preserve* the high 4 bits
if (fatType === 'fat12') fatChain.readFromPosition(info, info.struct.size, function (e,n,d) {
var value = info.struct.valueFromBytes(d);
if (clusterNum % 2) {
value.field1ab = status >>> 4;
value.field1c = status & 0x0F;
} else {
value.field0a = status >>> 8;
value.field0bc = status & 0xFF;
}
var entry = info.struct.bytesFromValue(value);
fatChain.writeToPosition(info, entry, cb);
}); else {
var entry = info.struct.bytesFromValue(status);
fatChain.writeToPosition(info, entry, cb);
}
};
vol.allocateInFAT = function (hint, cb) {
if (typeof hint === 'function') {
cb = hint;
hint = 2; // TODO: cache a better starting point?
}
function searchForFreeCluster(num, cb) {
if (num < countofClusters) vol.fetchFromFAT(num, function (e, status) {
if (e) cb(e);
else if (status === 'free') cb(null, num);
else searchForFreeCluster(num+1, cb);
}); else cb(S.err.NOSPC()); // TODO: try searching backwards from hint…
}
searchForFreeCluster(hint, function (e, clusterNum) {
if (e) cb(e);
else vol.storeToFAT(clusterNum, 'eof', cb.bind(null,null,clusterNum));
});
};
vol.rootDirectoryChain = (isFAT16) ?
c.sectorChain(vol, firstDataSector - rootDirSectors, rootDirSectors) :
c.clusterChain(vol, BS.RootClus);
vol.rootDirectoryChain.cacheAdvice = 'WILLNEED';
vol.chainForCluster = c.clusterChain.bind(c, vol);
vol.chainFromJSON = function (d) {
return ('numSectors' in d) ?
c.sectorChain(vol, d.firstSector, d.numSectors) :
c.clusterChain(vol, d.firstCluster);
};
return vol;
}

View File

@@ -19,12 +19,12 @@
},
"main": "./index.mjs",
"dependencies": {
"@xen-orchestra/fs": "^4.3.0",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/fs": "^4.2.0",
"@xen-orchestra/log": "^0.7.0",
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.11.2"
"vhd-lib": "^4.11.1"
},
"bin": {
"xo-fuse-vhd": "./cli.mjs"

View File

@@ -34,8 +34,9 @@ export default class MultiNbdClient {
async connect() {
const candidates = [...this.#settings]
const promises = []
const baseOptions = this.#options
const _connect = async () => {
const _connect = async () => {
if (candidates.length === 0) {
return
}
@@ -62,11 +63,11 @@ export default class MultiNbdClient {
return _connect()
}
}
// don't connect in parallel since this can lead to race condition
// on distributed systems ( like the NBD server of the XAPI)
for (let i = 0; i < this.#nbdConcurrency; i++) {
await _connect()
promises.push(_connect())
}
await Promise.all(promises)
if (this.#clients.length === 0) {
throw new Error(`Fail to connect to any Nbd client`)
}

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "3.1.2",
"version": "3.1.1",
"engines": {
"node": ">=14.0"
},
@@ -22,9 +22,9 @@
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/log": "^0.7.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^4.7.0"
"xen-api": "^4.4.0"
},
"devDependencies": {
"tap": "^18.7.0",

View File

@@ -11,7 +11,7 @@
},
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/log": "^0.7.0",
"axios": "^1.4.0",
"soap": "^1.0.0",
"soap-cookie": "^0.10.1"

View File

@@ -151,7 +151,7 @@ describe('Task', function () {
it('throws if the task has not started yet', function () {
const task = createTask()
assert.throws(() => task.failure(error), { message: /^task has not started yet\b/ })
assert.throws(() => task.failure(error), { message: 'task has not started yet' })
})
it('throws if the task is running', function () {
@@ -262,7 +262,7 @@ describe('Task', function () {
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task.set(name, value), { message: /^task has not started yet\b/ })
assert.throws(() => task.set(name, value), { message: 'task has not started yet' })
})
it(`emits an property message`, async function () {
@@ -303,7 +303,7 @@ describe('Task', function () {
it('throws if the task has not started yet', function () {
const task = createTask()
assert.throws(() => task.success(result), { message: /^task has not started yet\b/ })
assert.throws(() => task.success(result), { message: 'task has not started yet' })
})
it('throws if the task is running', function () {
@@ -356,7 +356,7 @@ describe('Task', function () {
describe(`#${type}()`, function () {
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task[type]('foo'), { message: /^task has not started yet\b/ })
assert.throws(() => task[type]('foo'), { message: 'task has not started yet' })
})
it(`emits an ${type} message`, async function () {

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.6.0",
"version": "0.5.0",
"engines": {
"node": ">=14"
},

View File

@@ -17,7 +17,7 @@
},
"dependencies": {
"@vates/decorate-with": "^2.1.0",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/log": "^0.7.0",
"golike-defer": "^0.5.1",
"object-hash": "^2.0.1"
},

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.57.0",
"@xen-orchestra/fs": "^4.3.0",
"@xen-orchestra/backups": "^0.54.1",
"@xen-orchestra/fs": "^4.2.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.28",
"version": "1.0.25",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -1,10 +0,0 @@
## Changing the default backup configuration
This library receives a backup configuration from `xo-server` or `xo-proxy`. This configuration is split between multiple properties:
- `config` contains values related to the general backup configuration
- `config.defaultSettings`, `config.vm.defaultSettings` and `config.metadata.defaultSettings` contain values related to backup jobs default settings:
- `config.vm.defaultSettings` is related to VM backup jobs (see [DEFAULT_XAPI_VM_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/VmsRemote.mjs#L15))
- `config.metadata.defaultSettings` is related to metadata backup jobs (see [DEFAULT_METADATA_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/Metadata.mjs#L12))
- `config.defaultSettings` is related to all backup jobs (see [DEFAULT_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/_Abstract.mjs#L7))
- in case of duplicate value, `config.vm.defaultSettings` and `config.metadata.defaultSettings` will prevail over `config.defaultSettings`, and a defined job setting will prevail over any `defaultSettings`

View File

@@ -59,8 +59,8 @@ export class HealthCheckVmBackup {
const running = new Date()
remainingTimeout -= running - started
// wait for the guest tools to be detected
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_detected, {
// wait for the guest tool version to be defined
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
timeout: remainingTimeout,
timeoutMessage: refOrUuid =>
`timeout reached while waiting for ${refOrUuid} to report the driver version through the Xen tools. Please check or update the Xen tools.`,

View File

@@ -58,7 +58,7 @@ export class ImportVmBackup {
async _reuseNearestSnapshot($defer, ignoredVdis) {
const metadata = this._metadata
const { mapVdisSrs } = this._importIncrementalVmSettings
const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
const streams = {}
const metdataDir = dirname(metadata._filename)
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
@@ -79,13 +79,8 @@ export class ImportVmBackup {
debug('found disks, wlll search its snapshots', { snapshots: xapiDisk.snapshots })
for (const snapshotRef of xapiDisk.snapshots) {
const snapshot = await this._xapi.getRecord('VDI', snapshotRef)
debug('handling snapshot', { snapshot })
if (snapshot.type === 'cbt_metadata') {
// disk without data can't be used as a base
debug('cbt metadata snapshot, skip')
continue
}
// take only the first snapshot
if (snapshotCandidate && snapshotCandidate.snapshot_time < snapshot.snapshot_time) {
debug('already got a better candidate')
@@ -191,7 +186,6 @@ export class ImportVmBackup {
version: '1.0.0',
vifs,
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
vtpms,
}
}

View File

@@ -12,19 +12,6 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backu
npm install --save @xen-orchestra/backups
```
## Usage
## Changing the default backup configuration
This library receives a backup configuration from `xo-server` or `xo-proxy`. This configuration is split between multiple properties:
- `config` contains values related to the general backup configuration
- `config.defaultSettings`, `config.vm.defaultSettings` and `config.metadata.defaultSettings` contain values related to backup jobs default settings:
- `config.vm.defaultSettings` is related to VM backup jobs (see [DEFAULT_XAPI_VM_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/VmsRemote.mjs#L15))
- `config.metadata.defaultSettings` is related to metadata backup jobs (see [DEFAULT_METADATA_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/Metadata.mjs#L12))
- `config.defaultSettings` is related to all backup jobs (see [DEFAULT_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/_Abstract.mjs#L7))
- in case of duplicate value, `config.vm.defaultSettings` and `config.metadata.defaultSettings` will prevail over `config.defaultSettings`, and a defined job setting will prevail over any `defaultSettings`
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -744,7 +744,7 @@ export class RemoteAdapter {
async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true } = {}) {
const handler = this._handler
const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
const dir = dirname(metadata._filename)
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
@@ -760,7 +760,6 @@ export class RemoteAdapter {
version: '1.0.0',
vifs,
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
vtpms,
}
}

View File

@@ -1,136 +1,4 @@
import moment from 'moment-timezone'
import assert from 'node:assert'
function instantiateTimezonedDateCreator(timezone) {
return date => {
const transformed = timezone ? moment.tz(date, timezone) : moment(date)
assert.ok(transformed.isValid(), `date ${date} , timezone ${timezone} is invalid`)
return transformed
}
}
const LTR_DEFINITIONS = {
daily: {
makeDateFormatter: ({ firstHourOfTheDay = 0, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.hour(copy.hour() - firstHourOfTheDay)
return copy.format('YYYY-MM-DD')
}
},
},
weekly: {
makeDateFormatter: ({ firstDayOfWeek = 0 /* relative to timezone week start */, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.date(date.date() - firstDayOfWeek)
// warning, the year in term of week may different from YYYY
// since the computation of the first week of a year is timezone dependant
return copy.format('gggg-WW')
}
},
ancestor: 'daily',
},
monthly: {
makeDateFormatter: ({ firstDayOfMonth = 0, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.date(copy.date() - firstDayOfMonth)
return copy.format('YYYY-MM')
}
},
ancestor: 'weekly',
},
yearly: {
makeDateFormatter: ({ firstDayOfYear = 0, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.date(copy.date() - firstDayOfYear)
return copy.format('YYYY')
}
},
ancestor: 'monthly',
},
}
// returns all entries but the last retention-th
/**
* return the entries too old to be kept
* if multiple entries are i the same time bucket : keep only the most recent one
* if an entry is valid in any of the bucket OR the minRetentionCount : keep it
* if a bucket is completly empty : it does not count as one, thus it may extend the retention
* @returns Array<Backup>
*/
export function getOldEntries(minRetentionCount, entries, { longTermRetention = {}, timezone } = {}) {
assert.strictEqual(
typeof minRetentionCount,
'number',
`minRetentionCount must be a number, got ${JSON.stringify(minRetentionCount)}`
)
assert.strictEqual(
minRetentionCount >= 0,
true,
`minRetentionCount must be a positive number, got ${JSON.stringify(minRetentionCount)}`
)
const dateBuckets = {}
const dateCreator = instantiateTimezonedDateCreator(timezone)
// only check buckets that have a retention set
for (const [duration, { retention, settings }] of Object.entries(longTermRetention)) {
assert.notStrictEqual(LTR_DEFINITIONS[duration], undefined, `Retention of type ${duration} is not defined`)
assert.notStrictEqual(
timezone,
undefined,
`timezone must defined for ltr, got ${JSON.stringify({ minRetentionCount, longTermRetention, timezone })}`
)
assert.strictEqual(
typeof retention,
'number',
`retention of type ${duration} must be a number, got ${JSON.stringify(retention)}`
)
assert.strictEqual(
retention > 0,
true,
`retention of type ${duration} must be a positive number, got ${JSON.stringify(retention)}`
)
dateBuckets[duration] = {
remaining: retention,
lastMatchingBucket: null,
formatter: LTR_DEFINITIONS[duration].makeDateFormatter({ ...settings, dateCreator }),
}
}
const nb = entries.length
const oldEntries = []
for (let i = nb - 1; i >= 0; i--) {
const entry = entries[i]
const entryDate = dateCreator(entry.timestamp)
let shouldBeKept = false
for (const [duration, { remaining, lastMatchingBucket, formatter }] of Object.entries(dateBuckets)) {
if (remaining === 0) {
continue
}
const bucket = formatter(entryDate)
if (lastMatchingBucket !== bucket) {
if (lastMatchingBucket !== null) {
assert.strictEqual(
lastMatchingBucket > bucket,
true,
`entries must be sorted in asc order ${lastMatchingBucket} ${bucket}`
)
}
shouldBeKept = true
dateBuckets[duration].remaining -= 1
dateBuckets[duration].lastMatchingBucket = bucket
}
}
if (i >= nb - minRetentionCount) {
shouldBeKept = true
}
if (!shouldBeKept) {
oldEntries.push(entry)
}
}
// we expect the entries to be in the right order
return oldEntries.reverse()
export function getOldEntries(retention, entries) {
return entries === undefined ? [] : retention > 0 ? entries.slice(0, -retention) : entries
}

View File

@@ -1,282 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { getOldEntries } from './_getOldEntries.mjs'
describe('_getOldEntries() should succeed', () => {
const tests = [
{
args: [
1,
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
],
expectedIds: [1, 2],
testLabel: 'should handle number based retention ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-09-01 00:01:00'), id: 1 }, // too old
{ timestamp: +new Date('2024-09-01 00:00:00'), id: 2 }, // too old
{ timestamp: +new Date('2024-09-02 00:09:00'), id: 3 }, // too old in same day
{ timestamp: +new Date('2024-09-02 00:10:00'), id: 4 },
{ timestamp: +new Date('2024-09-03 00:09:00'), id: 5 },
{ timestamp: +new Date('2024-09-04 00:09:00'), id: 6 }, // too old in same day
{ timestamp: +new Date('2024-09-04 00:10:00'), id: 7 },
],
{
longTermRetention: {
daily: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
expectedIds: [1, 2, 3, 6],
testLabel: 'should handle day based retention ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-09-01 00:01:00'), id: 1 }, // week n-3 too old
{ timestamp: +new Date('2024-09-02 00:00:00'), id: 2 }, // week n-3 too old
{ timestamp: +new Date('2024-09-03 00:09:00'), id: 3 }, // week n-2
{ timestamp: +new Date('2024-09-04 00:09:00'), id: 4 }, // week n-2
{ timestamp: +new Date('2024-09-05 00:09:00'), id: 5 }, // week n-2
{ timestamp: +new Date('2024-09-06 00:09:00'), id: 6 }, // week n-2
{ timestamp: +new Date('2024-09-07 00:09:00'), id: 7 }, // week n-2, most recent kept
{ timestamp: +new Date('2024-09-09 00:09:00'), id: 8 }, // week n-1, too old
{ timestamp: +new Date('2024-09-15 00:09:00'), id: 9 }, // week n-1 kept
{ timestamp: +new Date('2024-09-22 00:09:00'), id: 10 }, // week n kept
],
{
longTermRetention: {
weekly: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
expectedIds: [1, 2, 3, 4, 5, 6, 8],
testLabel: 'should handle week based retention ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-06-22 00:09:00'), id: 1 }, // too old
{ timestamp: +new Date('2024-07-31 00:09:00'), id: 2 }, // first of july
{ timestamp: +new Date('2024-08-01 00:09:00'), id: 3 }, // older of august
{ timestamp: +new Date('2024-08-05 00:09:00'), id: 4 }, // older of august
{ timestamp: +new Date('2024-08-07 00:09:00'), id: 5 }, // most recent of august
{ timestamp: +new Date('2024-09-09 00:09:00'), id: 6 }, // older of september
{ timestamp: +new Date('2024-09-15 00:09:00'), id: 7 }, // older of september
{ timestamp: +new Date('2024-09-22 00:09:00'), id: 8 }, // most recent of september
],
{
longTermRetention: {
monthly: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
expectedIds: [1, 3, 4, 6, 7],
testLabel: 'should handle month based retention',
},
{
args: [
0,
[
{ timestamp: +new Date('2023-05-18 00:09:00'), id: 1 }, // too old
{ timestamp: +new Date('2024-06-15 00:09:00'), id: 2 }, // too old in same year
{ timestamp: +new Date('2024-07-04 00:09:00'), id: 3 }, // too old
{ timestamp: +new Date('2024-08-12 00:09:00'), id: 4 }, // too old
{ timestamp: +new Date('2024-09-05 00:09:00'), id: 5 }, // too old
{ timestamp: +new Date('2024-10-02 00:09:00'), id: 6 }, // new month,
{ timestamp: +new Date('2024-11-01 00:09:00'), id: 7 }, // new month , week reached retention
{ timestamp: +new Date('2024-12-17 00:09:00'), id: 8 }, // new week
{ timestamp: +new Date('2024-12-24 00:09:00'), id: 9 }, // new week/month / year daily reach retention
{ timestamp: +new Date('2025-01-01 00:09:00'), id: 10 }, // same day/week/month/year
{ timestamp: +new Date('2025-01-01 00:10:00'), id: 11 }, // new day / week / month
{ timestamp: +new Date('2025-12-31 00:09:00'), id: 12 }, // same day/week/month/year
{ timestamp: +new Date('2025-12-31 00:09:00'), id: 13 }, // new month /year
],
{
longTermRetention: {
daily: { retention: 2 },
weekly: { retention: 4 },
monthly: { retention: 5 },
yearly: { retention: 2 },
},
timezone: 'Europe/Paris', // use a time zone here because week definition is timezone bound
},
],
expectedIds: [1, 2, 3, 4, 5, 10, 12],
testLabel: 'complete test ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-09-01 00:01:00'), id: 1 }, // thrid day too old
{ timestamp: +new Date('2024-09-01 00:10:00'), id: 2 }, // thrid day
{ timestamp: +new Date('2024-09-01 00:20:00'), id: 3 }, // second day, too old
{ timestamp: +new Date('2024-09-02 00:22:00'), id: 4 }, // second day too old
{ timestamp: +new Date('2024-09-03 00:20:00'), id: 5 }, // second day in NZ
{ timestamp: +new Date('2024-09-04 00:09:00'), id: 6 }, // same day in NZ
{ timestamp: +new Date('2024-09-04 00:10:00'), id: 7 }, // most recent
],
{
longTermRetention: {
daily: { retention: 3 },
},
timezone: 'Pacific/Auckland', // GMT +6
},
],
expectedIds: [1, 2, 3, 6],
testLabel: 'should handle timezone ',
},
]
for (const { args, expectedIds, testLabel } of tests) {
it(testLabel, () => {
const oldEntries = getOldEntries.apply(null, args)
assert.strictEqual(
oldEntries.length,
expectedIds.length,
`different length , ${JSON.stringify({ oldEntries, expectedIds })}`
)
for (let i = 0; i < expectedIds.length; i++) {
assert.strictEqual(
oldEntries[i].id,
expectedIds[i],
`different id , ${JSON.stringify({ i, expectedIds, oldEntries })}`
)
}
})
}
})
describe('_getOldEntries() should fail when called incorrectly', () => {
const tests = [
{
args: [
-1,
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
],
testLabel: 'negative min retention ',
},
{
args: [
{ not: 'a number' },
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
],
testLabel: 'non number min retention ',
},
{
args: [
4,
[
{ timestamp: 1, id: 1 },
{ timestamp: 2, id: 2 },
{ timestamp: 3, id: 3 },
],
{
longTermRetention: {
invalidType: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'invalid type ltr ',
},
{
args: [
3,
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
{
longTermRetention: {
daily: { retention: 0 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'zero retention ltr ',
},
{
args: [
1,
[
{ timestamp: 1, id: 1 },
{ timestamp: 2, id: 2 },
{ timestamp: 3, id: 3 },
],
{
longTermRetention: {
daily: { retention: 'not a number' },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'no timezone ltr ',
},
{
args: [
5,
[
{ timestamp: +new Date('2024-09-01'), id: 1 },
{ timestamp: +new Date('2024-09-03'), id: 3 },
{ timestamp: +new Date('2024-09-02'), id: 2 },
],
{
longTermRetention: {
daily: { retention: 5 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'unsorted entries ',
},
{
args: [
5,
[
{ timestamp: 'notadate', id: 1 },
{ timestamp: 'still not a date, but after', id: 3 },
],
{
longTermRetention: {
daily: { retention: 5 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'broken date ',
},
]
for (const { args, testLabel } of tests) {
it(testLabel, () => {
assert.throws(() => getOldEntries.apply(null, args), { code: 'ERR_ASSERTION' })
})
}
})

View File

@@ -104,18 +104,6 @@ export async function exportIncrementalVm(
}
})
const vtpms = await Promise.all(
vm.$VTPMs.map(async vtpm => {
let content
try {
content = await vm.$xapi.call('VTPM.get_contents', vtpm.$ref)
} catch (err) {
console.error(err)
}
return content
})
)
return Object.defineProperty(
{
version: '1.1.0',
@@ -125,7 +113,6 @@ export async function exportIncrementalVm(
vm: {
...vm,
},
vtpms,
},
'streams',
{
@@ -294,12 +281,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
}
}),
])
// recreate VTPMs
await Promise.all(
(incrementalVm.vtpms ?? []).map(async contents => {
await xapi.VTPM_create({ VM: vmRef, contents })
})
)
await Promise.all([
incrementalVm.vm.ha_always_run && xapi.setField('VM', vmRef, 'ha_always_run', true),

View File

@@ -91,11 +91,9 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
'writer.updateUuidAndChain()'
)
if (isFull) {
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, 0)
} else {
if (!isFull) {
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, (this._deltaChainLength ?? 0) + 1)
}
} // on a full the delta chain will be null
// not the case if offlineBackup
if (exportedVm.is_a_snapshot) {

View File

@@ -74,12 +74,9 @@ export const Abstract = class AbstractVmBackupRunner {
// check if current VM has tags
const tags = this._tags
const intersect = settings.healthCheckVmsWithTags.some(t => tags.includes(t))
// accept both 'xo:no-health-check' and 'xo:no-health-check=reason'
const vmAlwaysIgnored = tags.some(t => t.startsWith('xo:no-health-check'))
const intersect = !vmAlwaysIgnored && settings.healthCheckVmsWithTags.some(t => tags.includes(t))
if (vmAlwaysIgnored || (settings.healthCheckVmsWithTags.length !== 0 && !intersect)) {
if (settings.healthCheckVmsWithTags.length !== 0 && !intersect) {
// create a task to have an info in the logs and reports
return Task.run(
{

View File

@@ -150,7 +150,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
try {
// enable CBT on all disks if possible
const diskRefs = await xapi.VM_getDisks(vm.$ref)
await Promise.all(diskRefs.map(diskRef => xapi.callAsync('VDI.enable_cbt', diskRef)))
await Promise.all(diskRefs.map(diskRef => xapi.call('VDI.enable_cbt', diskRef)))
} catch (error) {
Task.info(`couldn't enable CBT`, error)
}

View File

@@ -38,8 +38,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
const oldBackups = getOldEntries(
settings.exportRetention - 1,
await adapter.listVmBackups(vm.uuid, _ => _.mode === 'full' && _.scheduleId === scheduleId),
{ longTermRetention: settings.longTermRetention, timezone: settings.timezone }
await adapter.listVmBackups(vm.uuid, _ => _.mode === 'full' && _.scheduleId === scheduleId)
)
const deleteOldBackups = () => adapter.deleteFullVmBackups(oldBackups)

View File

@@ -95,8 +95,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
const oldEntries = getOldEntries(
settings.exportRetention - 1,
await adapter.listVmBackups(vmUuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId),
{ longTermRetention: settings.longTermRetention, timezone: settings.timezone }
await adapter.listVmBackups(vmUuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId)
)
this._oldEntries = oldEntries
@@ -221,7 +220,6 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
vhds,
vm,
vmSnapshot,
vtpms: deltaExport.vtpms,
}
const { size } = await Task.run({ name: 'transfer' }, async () => {

View File

@@ -58,8 +58,8 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
const { disableMergeWorker } = this._config
// merge worker only compatible with local remotes
const { handler } = this._adapter
const synchronousMerge = this._settings.mergeBackupsSynchronously
const willMergeInWorker = !synchronousMerge && !disableMergeWorker && typeof handler.getRealPath === 'function'
const willMergeInWorker = !disableMergeWorker && typeof handler.getRealPath === 'function'
const { merge } = await this._cleanVm({ remove: true, merge: !willMergeInWorker })
await this.#lock.dispose()

View File

@@ -65,7 +65,7 @@ In case any incoherence is detected, the file is deleted so it will be fully gen
- `xo:backup:deltaChainLength` = n (number of delta copies/replicated since a full)
- `xo:backup:exported` = 'true' (added at the end of the backup)
### Of created VMs , their associated VDIs and snapshots
### Of created VMs , their associated VDIs and snapshots
- `other_config`:
- `xo:backup:datetime`: format is UTC %Y%m%dT%H:%M:%SZ

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.57.0",
"version": "0.54.1",
"engines": {
"node": ">=14.18"
},
@@ -25,11 +25,11 @@
"@vates/decorate-with": "^2.1.0",
"@vates/disposable": "^0.1.6",
"@vates/fuse-vhd": "^2.1.2",
"@vates/nbd-client": "^3.1.2",
"@vates/nbd-client": "^3.1.1",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.3.0",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/fs": "^4.2.0",
"@xen-orchestra/log": "^0.7.0",
"@xen-orchestra/template": "^0.1.0",
"app-conf": "^3.0.0",
"compare-versions": "^6.0.0",
@@ -39,7 +39,6 @@
"human-format": "^1.2.0",
"limit-concurrency-decorator": "^0.6.0",
"lodash": "^4.17.20",
"moment-timezone": "^0.5.46",
"ms": "^2.1.3",
"node-zone": "^0.4.0",
"parse-pairs": "^2.0.0",
@@ -48,8 +47,8 @@
"tar": "^6.1.15",
"uuid": "^9.0.0",
"value-matcher": "^0.2.0",
"vhd-lib": "^4.11.2",
"xen-api": "^4.7.0",
"vhd-lib": "^4.11.1",
"xen-api": "^4.4.0",
"yazl": "^2.5.1"
},
"devDependencies": {
@@ -59,7 +58,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^7.10.0"
"@xen-orchestra/xapi": "^7.6.1"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^4.7.0"
"xen-api": "^4.4.0"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -21,7 +21,6 @@ npm install --save @xen-orchestra/defined
Returns the first non-`undefined` value from a list of arguments, evaluating functions if needed.
There is two ways to use this function:
- either with a single array argument: it should return the first non-undefined item
- or with multiple arguments: it should return the first non-undefined argument

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "4.3.0",
"version": "4.2.0",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
@@ -29,7 +29,7 @@
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.1.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/log": "^0.7.0",
"bind-property-descriptor": "^2.0.0",
"decorator-synchronized": "^0.6.0",
"execa": "^5.0.0",
@@ -41,7 +41,7 @@
"proper-lockfile": "^4.1.2",
"readable-stream": "^4.1.0",
"through2": "^4.0.2",
"xo-remote-parser": "^0.9.3"
"xo-remote-parser": "^0.9.2"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -99,7 +99,7 @@ export default class RemoteHandlerAbstract {
this.closeFile = sharedLimit(this.closeFile)
this.copy = sharedLimit(this.copy)
this.getInfo = sharedLimit(this.getInfo)
this.getSizeOnDisk = sharedLimit(this.getSizeOnDisk)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
this.mkdir = sharedLimit(this.mkdir)
this.openFile = sharedLimit(this.openFile)
@@ -227,15 +227,13 @@ export default class RemoteHandlerAbstract {
return timeout.call(this._getInfo(), this._timeout)
}
// returns the real size occupied by an unencrypted file
// encrypted files have metadata and padding that blur the real size
// when using encryption, the file size is aligned with the encryption block size ( 16 bytes )
// that means that the size will be 1 to 16 bytes more than the content size + the initialized vector length (16 bytes)
async getSize(file) {
assert.strictEqual(this.isEncrypted, false, `Can't compute size of an encrypted file ${file}`)
return this.getSizeOnDisk(file)
}
async getSizeOnDisk(file) {
return timeout.call(this._getSize(typeof file === 'string' ? normalizePath(file) : file), this._timeout)
const size = await timeout.call(this._getSize(typeof file === 'string' ? normalizePath(file) : file), this._timeout)
return size - this.#encryptor.ivLength
}
async __list(dir, { filter, ignoreMissing = false, prependDir = false } = {}) {
@@ -390,11 +388,6 @@ export default class RemoteHandlerAbstract {
info('will update metadata of this remote')
return this.#createMetadata()
} else {
// to add a new encrypted fs remote, the remote directory must be empty, otherwise metadata.json is not created
if (error.code === 'ENOENT' && error.path.includes('metadata.json')) {
throw new Error('Remote directory must be empty.')
}
warn(
`The encryptionKey settings of this remote does not match the key used to create it. You won't be able to read any data from this remote`,
{ error }

View File

@@ -34,7 +34,6 @@ const rejectionOf = p =>
reason => reason
)
// TODO : add tests on encrypted remote
const handlers = [`file://${tmpdir()}`]
if (process.env.xo_fs_nfs) handlers.push(process.env.xo_fs_nfs)
if (process.env.xo_fs_smb) handlers.push(process.env.xo_fs_smb)
@@ -98,12 +97,11 @@ handlers.forEach(url => {
})
})
// TODO : add test "should throw if remote is encrypted" on #getSize()
describe('#getSizeOnDisk()', () => {
describe('#getSize()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
testWithFileDescriptor('file', 'r', async () => {
assert.equal(await handler.getSizeOnDisk('file'), TEST_DATA_LEN)
assert.equal(await handler.getSize('file'), TEST_DATA_LEN)
})
})

View File

@@ -58,7 +58,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
constructor(remote, opts = {}) {
super(remote)
this.#addSyncStackTrace = (opts.syncStackTraces ?? true) ? addSyncStackTrace : dontAddSyncStackTrace
this.#addSyncStackTrace = opts.syncStackTraces ?? true ? addSyncStackTrace : dontAddSyncStackTrace
this.#retriesOnEagain = {
delay: 1e3,
retries: 9,

View File

@@ -7,8 +7,7 @@ export default class NfsHandler extends MountHandler {
const { host, port, path } = parse(remote.url)
super(remote, opts, {
type: 'nfs',
device: `${host}:${path}`,
options: port !== undefined ? `port=${port}` : undefined,
device: `${host}${port !== undefined ? ':' + port : ''}:${path}`,
})
}

View File

@@ -17,19 +17,19 @@
"xo-lift-remote-immutability": "./liftProtection.mjs"
},
"license": "AGPL-3.0-or-later",
"version": "1.0.16",
"version": "1.0.13",
"engines": {
"node": ">=14.0.0"
},
"dependencies": {
"@vates/async-each": "^1.0.0",
"@xen-orchestra/backups": "^0.57.0",
"@xen-orchestra/log": "^0.7.1",
"@xen-orchestra/backups": "^0.54.1",
"@xen-orchestra/log": "^0.7.0",
"app-conf": "^3.0.0",
"chokidar": "^3.5.3",
"execa": "^5.0.0",
"ms": "^2.1.3",
"vhd-lib": "^4.11.2"
"vhd-lib": "^4.11.1"
},
"devDependencies": {
"rimraf": "^5.0.5",

View File

@@ -2,18 +2,6 @@
## **next**
- Fix persistent scrollbar on the right (PR [#8191](https://github.com/vatesfr/xen-orchestra/pull/8191))
## **0.6.0** (2024-11-29)
- [Pool/Dashboard] `Patches` and `Status` cards are displayed with a greater minimum width (PR [#8108](https://github.com/vatesfr/xen-orchestra/pull/8108))
- [Header] Update user menu button (PR [#8109](https://github.com/vatesfr/xen-orchestra/pull/8109))
- [VM/Console] Display _Console Clipboard_ and _Console Actions_ (PR [#8125](https://github.com/vatesfr/xen-orchestra/pull/8125))
- [i18n] Add Czech translation (contribution made by [@p-bo](https://github.com/p-bo)) (PR [#8098](https://github.com/vatesfr/xen-orchestra/pull/8098))
- [Host/Console] Display _Console_, _Console Clipboard_ and _Console Actions_ (PR [#8136](https://github.com/vatesfr/xen-orchestra/pull/8136))
## **0.5.0** (2024-10-31)
- Handle IPv6 hosts (PR [#8044](https://github.com/vatesfr/xen-orchestra/pull/8044))
- [XOA deploy] Configure custom NTP servers (PR [#8059](https://github.com/vatesfr/xen-orchestra/pull/8059))

View File

@@ -7,4 +7,4 @@
## Guidelines
Please follow the guidelines from [Web Core documentation](../web-core/docs/index.md)
Please follow the guidelines from the `@xen-orchestra/web-core/docs/guidelines` directory.

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.6.0",
"version": "0.4.0",
"type": "module",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
@@ -15,7 +15,6 @@
},
"devDependencies": {
"@csstools/postcss-global-data": "^2.1.1",
"@floating-ui/vue": "^1.1.5",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
@@ -27,19 +26,19 @@
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.7",
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/tsconfig": "^0.5.1",
"@vueuse/core": "^10.7.1",
"@vueuse/math": "^10.7.1",
"@vueuse/shared": "^10.7.1",
"@xen-orchestra/web-core": "^0.10.0",
"@xen-orchestra/web-core": "^0.3.0",
"complex-matcher": "^0.7.1",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.4.3",
"file-saver": "^2.0.5",
"highlight.js": "^11.9.0",
"human-format": "^1.2.1",
"human-format": "^1.2.0",
"iterable-backoff": "^0.1.0",
"json-rpc-2.0": "^1.7.0",
"json5": "^2.2.3",
@@ -49,19 +48,19 @@
"marked": "^9.1.5",
"minimist": "^1.2.8",
"npm-run-all": "^4.1.5",
"pinia": "^2.2.6",
"pinia": "^2.1.7",
"placement.js": "^1.0.0-beta.5",
"postcss": "^8.4.33",
"postcss-color-function": "^4.1.0",
"postcss-custom-media": "^10.0.2",
"postcss-nested": "^6.0.1",
"typescript": "~5.6.3",
"vite": "^5.4.10",
"vue": "~3.5.12",
"typescript": "~5.3.3",
"vite": "^5.0.11",
"vue": "~3.4.13",
"vue-echarts": "^6.6.8",
"vue-i18n": "^9.9.0",
"vue-router": "^4.4.0",
"vue-tsc": "^2.1.10",
"vue-tsc": "^1.8.27",
"zx": "^7.2.3"
},
"private": true,

View File

@@ -1,8 +1,8 @@
<template>
<div v-if="!route.meta.hasStoryNav && !xenApiStore.isConnected">
<div v-if="!$route.meta.hasStoryNav && !xenApiStore.isConnected">
<AppLogin />
</div>
<div v-else class="layout">
<div v-else>
<AppHeader v-if="uiStore.hasUi" />
<div class="container">
<AppNavigation v-if="uiStore.hasUi" />
@@ -10,7 +10,7 @@
<RouterView />
</main>
</div>
<VtsTooltipList />
<TooltipList />
</div>
<ModalList />
</template>
@@ -24,15 +24,12 @@ import { useChartTheme } from '@/composables/chart-theme.composable'
import { useUnreachableHosts } from '@/composables/unreachable-hosts.composable'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import VtsTooltipList from '@core/components/tooltip-list/VtsTooltipList.vue'
import TooltipList from '@core/components/tooltip/TooltipList.vue'
import { useUiStore } from '@core/stores/ui.store'
import { useActiveElement, useMagicKeys, whenever } from '@vueuse/core'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
const route = useRoute()
const xenApiStore = useXenApiStore()
@@ -70,12 +67,6 @@ useUnreachableHosts()
</script>
<style lang="postcss" scoped>
.layout {
display: flex;
flex-direction: column;
height: 100dvh;
}
.container {
display: flex;
}

View File

@@ -0,0 +1,91 @@
<template>
<MenuList placement="bottom-end" border>
<template #trigger="{ open, isOpen }">
<button type="button" :class="{ active: isOpen }" class="account-button" @click="open">
<UiIcon :icon="faCircleUser" class="user-icon" />
<UiIcon :icon="faAngleDown" class="dropdown-icon" />
</button>
</template>
<MenuItem :icon="faGear" @click="openSettings">{{ $t('settings') }}</MenuItem>
<MenuItem :icon="faMessage" @click="openFeedbackUrl">
{{ $t('send-us-feedback') }}
</MenuItem>
<MenuItem :icon="faArrowRightFromBracket" class="menu-item-logout" @click="logout">
{{ $t('log-out') }}
</MenuItem>
</MenuList>
</template>
<script lang="ts" setup>
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useXenApiStore } from '@/stores/xen-api.store'
import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import {
faAngleDown,
faArrowRightFromBracket,
faCircleUser,
faGear,
faMessage,
} from '@fortawesome/free-solid-svg-icons'
import { nextTick } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const logout = () => {
const xenApiStore = useXenApiStore()
xenApiStore.disconnect()
nextTick(() => router.push({ name: 'home' }))
}
const openFeedbackUrl = () => {
window.open('https://xcp-ng.org/forum/topic/4731/xen-orchestra-lite', '_blank', 'noopener')
}
const openSettings = () => router.push({ name: 'settings' })
</script>
<style lang="postcss" scoped>
.account-button {
display: flex;
align-items: center;
padding: 1rem;
color: var(--color-neutral-txt-primary);
border: none;
border-radius: 0.8rem;
background-color: var(--color-neutral-background-secondary);
gap: 0.8rem;
&:disabled {
color: var(--color-neutral-border);
}
&:not(:disabled) {
cursor: pointer;
&:hover,
&:active,
&.active {
background-color: var(--color-neutral-background-primary);
}
&:active,
&.active {
color: var(--color-normal-txt-base);
}
}
}
.user-icon {
font-size: 2.4rem;
}
.dropdown-icon {
font-size: 1.6rem;
}
.menu-item-logout {
color: var(--color-danger-txt-base);
}
</style>

View File

@@ -9,13 +9,13 @@
<div class="right">
<PoolOverrideWarning as-tooltip />
<XoaButton v-if="isDesktop" />
<AccountMenu />
<AccountButton />
</div>
</header>
</template>
<script lang="ts" setup>
import AccountMenu from '@/components/account-menu/AccountMenu.vue'
import AccountButton from '@/components/AccountButton.vue'
import PoolOverrideWarning from '@/components/PoolOverrideWarning.vue'
import TextLogo from '@/components/TextLogo.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
@@ -37,13 +37,13 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore)
display: flex;
align-items: center;
justify-content: space-between;
height: 5.6rem;
padding: 0 1.6rem;
height: 5.5rem;
padding: 1rem;
border-bottom: 0.1rem solid var(--color-neutral-border);
background-color: var(--color-neutral-background-secondary);
img {
width: 3.7rem;
width: 4rem;
}
.text-logo {

View File

@@ -27,7 +27,7 @@
<FormCheckbox v-model="rememberMe" />
{{ $t('keep-me-logged') }}
</label>
<UiButton size="medium" accent="info" variant="primary" type="submit" :busy="isConnecting">
<UiButton size="medium" color="normal" level="primary" type="submit" :busy="isConnecting">
{{ $t('login') }}
</UiButton>
</template>
@@ -45,7 +45,7 @@ import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { XenApiError } from '@/libs/xen-api/xen-api.types'
import { usePageTitleStore } from '@/stores/page-title.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiButton from '@core/components/button/UiButton.vue'
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'
import { useLocalStorage, whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'

View File

@@ -60,7 +60,7 @@ useEventListener(
}
code:not(.hljs-code) {
background-color: var(--color-info-background-selected);
background-color: var(--color-normal-background-selected);
padding: 0.3rem 0.6rem;
border-radius: 0.6rem;
}
@@ -103,11 +103,11 @@ useEventListener(
background-color: transparent;
&:hover {
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
}
&:active {
color: var(--color-info-txt-hover);
color: var(--color-normal-txt-hover);
}
}
}

View File

@@ -1,7 +1,7 @@
<template>
<Transition name="slide">
<nav v-if="isDesktop || isOpen" ref="navElement" :class="{ collapsible: isMobile }" class="app-navigation">
<StoryMenu v-if="route.meta.hasStoryNav" />
<StoryMenu v-if="$route.meta.hasStoryNav" />
<InfraPoolList v-else />
</nav>
</Transition>
@@ -15,9 +15,6 @@ import { useUiStore } from '@core/stores/ui.store'
import { onClickOutside, whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const uiStore = useUiStore()
const { isMobile, isDesktop } = storeToRefs(uiStore)
@@ -61,7 +58,6 @@ whenever(isOpen, () => {
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
transform: translateX(-37rem);

View File

@@ -1,12 +1,12 @@
<template>
<label class="form-widget">
<span v-if="label || slots.label" class="label">
<span v-if="label || $slots.label" class="label">
<slot name="label">
{{ label }}
</slot>
</span>
<span class="widget">
<span v-if="before || slots.before" class="before">
<span v-if="before || $slots.before" class="before">
<slot name="before">
<UiIcon v-if="isIcon(before)" :icon="before" fixed-width />
<template v-else>{{ before }}</template>
@@ -15,7 +15,7 @@
<span class="element">
<slot />
</span>
<span v-if="after || slots.after" class="after">
<span v-if="after || $slots.after" class="after">
<slot name="after">
<UiIcon v-if="isIcon(after)" :icon="after" fixed-width />
<template v-else>{{ after }}</template>
@@ -36,13 +36,6 @@ defineProps<{
inline?: boolean
}>()
const slots = defineSlots<{
default(): any
label?(): any
before?(): any
after?(): any
}>()
const isIcon = (maybeIcon: any): maybeIcon is IconDefinition => typeof maybeIcon === 'object'
</script>
@@ -68,7 +61,7 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition => typeof maybeIcon
gap: 0.1rem;
&:focus-within {
outline: 1px solid var(--color-info-item-active);
outline: 1px solid var(--color-normal-item-active);
}
}
@@ -78,7 +71,7 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition => typeof maybeIcon
}
.form-widget:hover .widget {
border-color: var(--color-info-item-hover);
border-color: var(--color-normal-item-hover);
}
.element {

View File

@@ -9,7 +9,7 @@
</th>
<td v-if="hasMultipleHosts">
<UiSpinner v-if="!areAllLoaded" />
<UiCounter
<VtsCounter
v-else
v-tooltip="{
placement: 'left',
@@ -33,7 +33,7 @@ import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import UiSpinner from '@/components/ui/UiSpinner.vue'
import UiTable from '@/components/ui/UiTable.vue'
import type { XenApiPatchWithHostRefs } from '@/composables/host-patches.composable'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import VtsCounter from '@core/components/counter/VtsCounter.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { useUiStore } from '@core/stores/ui.store'
import { computed } from 'vue'

View File

@@ -27,6 +27,6 @@
font-weight: 500;
font-size: 2rem;
line-height: 150%;
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
}
</style>

View File

@@ -40,7 +40,7 @@ const props = defineProps<{
}>()
const config: Config = {
host: { context: useHostStore().subscribe({ defer: true }), routeName: 'host.console' },
host: { context: useHostStore().subscribe({ defer: true }), routeName: 'host.dashboard' },
vm: { context: useVmStore().subscribe({ defer: true }), routeName: 'vm.console' },
sr: { context: useSrStore().subscribe({ defer: true }), routeName: undefined },
pool: { context: usePoolStore().subscribe({ defer: true }), routeName: 'pool.dashboard' },

View File

@@ -33,7 +33,7 @@ const isRecordNotFound = computed(() => props.isReady && !props.uuidChecker(id.v
}
.spinner {
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
display: flex;
margin: auto;
width: 10rem;

View File

@@ -26,7 +26,7 @@ const className = computed(() => `state-${props.state.toLocaleLowerCase()}`)
<style lang="postcss" scoped>
.power-state-icon {
color: var(--color-info-txt-item);
color: var(--color-normal-txt-item);
&.state-running {
color: var(--color-success-item-base);
@@ -37,7 +37,7 @@ const className = computed(() => `state-${props.state.toLocaleLowerCase()}`)
}
&.state-suspended {
color: var(--color-info-txt-hover);
color: var(--color-normal-txt-hover);
}
&.state-halted {

View File

@@ -110,16 +110,10 @@ defineExpose({
<style lang="postcss" scoped>
.vm-console {
flex: 1;
max-width: 100%;
height: 80rem;
& > :deep(div) {
background-color: transparent !important;
}
/* Required because the library adds "margin: auto" to the canvas which makes the canvas centered in space and not aligned to the rest of the layout */
:deep(canvas) {
margin: 0 auto !important;
}
}
</style>

View File

@@ -36,7 +36,7 @@ defineProps<{
.icon {
font-size: 2.5rem;
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
}
.title {

View File

@@ -60,20 +60,20 @@ const computedData = computed(() => {
}
.progress-item:nth-child(1) {
--progress-bar-color: var(--color-info-txt-item);
--progress-bar-color: var(--color-normal-txt-item);
}
.progress-item:nth-child(2) {
--progress-bar-color: var(--color-info-txt-active);
--progress-bar-color: var(--color-normal-txt-active);
}
.progress-item:nth-child(3) {
--progress-bar-color: var(--color-info-txt-hover);
--progress-bar-color: var(--color-normal-txt-hover);
}
.progress-item {
--progress-bar-height: 1.2rem;
--progress-bar-color: var(--color-info-item-hover);
--progress-bar-color: var(--color-normal-item-hover);
--progress-bar-background-color: var(--color-neutral-background-disabled);
&.warning {

View File

@@ -2,8 +2,8 @@
<UiButton
v-if="xoaFound"
size="medium"
accent="info"
variant="primary"
color="normal"
level="primary"
:left-icon="faArrowUpRightFromSquare"
class="xoa-button"
@click="openXoa()"
@@ -13,8 +13,8 @@
<UiButton
v-else
size="medium"
accent="info"
variant="primary"
color="normal"
level="primary"
:left-icon="faDownload"
class="xoa-button"
@click="openXoaDeploy()"
@@ -26,7 +26,7 @@
<script lang="ts" setup>
import type { PRIMARY_ADDRESS_TYPE } from '@/libs/xen-api/xen-api.enums'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiButton from '@core/components/button/UiButton.vue'
import { faDownload, faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import { useRouter } from 'vue-router'

View File

@@ -1,53 +0,0 @@
<template>
<UiAccountMenuButton
v-tooltip="menu.$isOpen ? false : { content: $t('settings'), placement: 'left' }"
size="medium"
v-bind="omit(menu.$trigger, ['as', 'submenu'])"
/>
<VtsMenuList border v-bind="menu.$target">
<VtsMenuItem :icon="faGear" v-bind="menu.settings">{{ $t('settings') }}</VtsMenuItem>
<VtsMenuItem :icon="faBook" v-bind="menu.documentation">
{{ $t('documentation-name', { name: 'XCP-ng' }) }}
</VtsMenuItem>
<VtsMenuItem :icon="faHeadset" v-bind="menu.support">{{ $t('professional-support') }}</VtsMenuItem>
<VtsMenuItem :icon="faComments" v-bind="menu.forum">{{ $t('access-forum') }}</VtsMenuItem>
<VtsMenuItem :icon="faArrowRightFromBracket" class="menu-item-logout" v-bind="menu.logout">
{{ $t('log-out') }}
</VtsMenuItem>
</VtsMenuList>
</template>
<script lang="ts" setup>
import { useXenApiStore } from '@/stores/xen-api.store'
import VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
import VtsMenuList from '@core/components/menu/VtsMenuList.vue'
import UiAccountMenuButton from '@core/components/ui/account-menu-button/UiAccountMenuButton.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { action, link, routerLink, useMenuToggle } from '@core/packages/menu'
import { faArrowRightFromBracket, faBook, faComments, faGear, faHeadset } from '@fortawesome/free-solid-svg-icons'
import { objectOmit as omit } from '@vueuse/shared'
import { useRouter } from 'vue-router'
const router = useRouter()
const menu = useMenuToggle({
placement: 'bottom-end',
items: {
settings: routerLink({ name: 'settings' }),
documentation: link('https://docs.xcp-ng.org?utm_campaign=xolite&utm_term=xcpdoc'),
support: link('https://vates.tech/pricing-and-support?utm_campaign=xolite&utm_term=pricing'),
forum: link('https://xcp-ng.org/forum/topic/4731/xen-orchestra-lite?utm_campaign=xolite&utm_term=forum'),
logout: action(async () => {
await useXenApiStore().disconnect()
void router.push({ name: 'home' })
}),
},
})
</script>
<style lang="postcss" scoped>
:deep(.menu-item-logout) {
color: var(--color-danger-txt-base);
}
</style>

View File

@@ -3,27 +3,27 @@
<TabItem v-bind="tab(TAB.PROPS, propParams)">Props</TabItem>
<TabItem class="event-tab" v-bind="tab(TAB.EVENTS, eventParams)">
Events
<UiCounter
<VtsCounter
v-if="unreadEventsCount > 0"
:value="unreadEventsCount"
accent="success"
size="small"
variant="primary"
size="small"
/>
</TabItem>
<TabItem v-bind="tab(TAB.SLOTS, slotParams)">Slots</TabItem>
<TabItem v-bind="tab(TAB.SETTINGS, settingParams)">Settings</TabItem>
<TabItem class="preset-tab" v-bind="presetMenu.$trigger">
<UiIcon :icon="faSliders" />
Presets
</TabItem>
<Teleport to="body">
<VtsMenuList border v-bind="presetMenu.$target">
<VtsMenuItem v-for="(preset, label) in presets" :key="label" v-bind="presetMenu[label]">
{{ label }}
</VtsMenuItem>
</VtsMenuList>
</Teleport>
<MenuList placement="bottom" border>
<template #trigger="{ open, isOpen }">
<TabItem :active="isOpen" :disabled="presets === undefined" class="preset-tab" @click="open">
<UiIcon :icon="faSliders" />
Presets
</TabItem>
</template>
<MenuItem v-for="(preset, label) in presets" :key="label" @click="applyPreset(preset)">
{{ label }}
</MenuItem>
</MenuList>
</TabList>
<div :class="{ 'full-width': fullWidthComponent }" class="tabs">
@@ -41,13 +41,7 @@
<UiCardTitle>
Logs
<template #right>
<UiButton
v-if="eventsLog.length > 0"
accent="info"
size="medium"
variant="tertiary"
@click="eventsLog = []"
>
<UiButton v-if="eventsLog.length > 0" size="medium" color="normal" level="tertiary" @click="eventsLog = []">
Clear
</UiButton>
</template>
@@ -91,13 +85,12 @@ import {
ModelParam,
type Param,
} from '@/libs/story/story-param'
import VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
import VtsMenuList from '@core/components/menu/VtsMenuList.vue'
import UiButton from '@core/components/button/UiButton.vue'
import VtsCounter from '@core/components/counter/VtsCounter.vue'
import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import TabItem from '@core/components/tab/TabItem.vue'
import TabList from '@core/components/tab/TabList.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import { action, useMenuToggle } from '@core/packages/menu'
import { faSliders } from '@fortawesome/free-solid-svg-icons'
import 'highlight.js/styles/github-dark.css'
import { uniqueId, upperFirst } from 'lodash-es'
@@ -267,17 +260,6 @@ const applyPreset = (preset: { props?: Record<string, any>; settings?: Record<st
})
}
}
const presetMenu = useMenuToggle({
placement: 'bottom-end',
items: computed(() =>
Object.fromEntries(
Object.entries(props.presets ?? {}).map(([label, preset]) => {
return [label, action(() => applyPreset(preset))]
})
)
),
})
</script>
<style lang="postcss" scoped>

View File

@@ -4,7 +4,7 @@
<span
v-if="node.children.size > 0"
class="directory typo p1-medium"
@click="emit('toggleDirectory', node.directory)"
@click="emit('toggle-directory', node.directory)"
>
<UiIcon :icon="isOpen(node.directory) ? faFolderOpen : faFolderClosed" />
{{ formatName(key) }}
@@ -18,7 +18,7 @@
v-if="isOpen(node.directory)"
:tree="node.children"
:opened-directories="openedDirectories"
@toggle-directory="emit('toggleDirectory', $event)"
@toggle-directory="emit('toggle-directory', $event)"
/>
</li>
</ul>
@@ -35,7 +35,7 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
toggleDirectory: [directory: string]
'toggle-directory': [directory: string]
}>()
const isOpen = (directory: string) => props.openedDirectories.has(directory)

View File

@@ -27,7 +27,7 @@
}
&:nth-child(odd) {
background-color: var(--color-info-background-selected);
background-color: var(--color-normal-background-selected);
}
}

View File

@@ -6,7 +6,7 @@
:disabled="isDisabled"
:type="type === 'radio' ? 'radio' : 'checkbox'"
class="input"
v-bind="attrs"
v-bind="$attrs"
/>
<span class="fake-checkbox">
<UiIcon :fixed-width="false" :icon class="icon" />
@@ -21,7 +21,7 @@ import { DisabledContext } from '@/context'
import { IK_CHECKBOX_TYPE, IK_FORM_HAS_LABEL } from '@/types/injection-keys'
import { faCheck, faCircle, faMinus } from '@fortawesome/free-solid-svg-icons'
import { useVModel } from '@vueuse/core'
import { computed, type HTMLAttributes, inject, useAttrs } from 'vue'
import { computed, type HTMLAttributes, inject } from 'vue'
defineOptions({ inheritAttrs: false })
@@ -38,8 +38,6 @@ const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const attrs = useAttrs()
const value = useVModel(props, 'modelValue', emit)
const type = inject(IK_CHECKBOX_TYPE, 'checkbox')
const hasLabel = inject(
@@ -83,7 +81,7 @@ const isIndeterminate = computed(() => (type === 'checkbox' || type === 'toggle'
.input.indeterminate + .fake-checkbox > .icon {
opacity: 1;
color: var(--color-info-txt-item);
color: var(--color-normal-txt-item);
}
}
@@ -145,7 +143,7 @@ const isIndeterminate = computed(() => (type === 'checkbox' || type === 'toggle'
.icon {
font-size: var(--checkbox-icon-size);
position: absolute;
color: var(--color-info-txt-item);
color: var(--color-normal-txt-item);
filter: drop-shadow(0 0.0625em 0.5em rgba(0, 0, 0, 0.1)) drop-shadow(0 0.1875em 0.1875em rgba(0, 0, 0, 0.06))
drop-shadow(0 0.1875em 0.25em rgba(0, 0, 0, 0.08));
@@ -164,7 +162,7 @@ const isIndeterminate = computed(() => (type === 'checkbox' || type === 'toggle'
background-color: var(--background-color);
box-shadow: var(--shadow-100);
--border-color: var(--color-info-item-base);
--border-color: var(--color-normal-item-base);
}
.input:disabled {
@@ -175,32 +173,32 @@ const isIndeterminate = computed(() => (type === 'checkbox' || type === 'toggle'
&:checked + .fake-checkbox {
--border-color: transparent;
--background-color: var(--color-info-item-disabled);
--background-color: var(--color-normal-item-disabled);
}
}
.input:not(:disabled) {
&:hover + .fake-checkbox,
&:focus + .fake-checkbox {
--border-color: var(--color-info-item-hover);
--border-color: var(--color-normal-item-hover);
}
&:active + .fake-checkbox {
--border-color: var(--color-info-item-active);
--border-color: var(--color-normal-item-active);
}
&:checked + .fake-checkbox {
--border-color: transparent;
--background-color: var(--color-info-item-base);
--background-color: var(--color-normal-item-base);
}
&:checked:hover + .fake-checkbox,
&:checked:focus + .fake-checkbox {
--background-color: var(--color-info-item-hover);
--background-color: var(--color-normal-item-hover);
}
&:checked:active + .fake-checkbox {
--background-color: var(--color-info-item-active);
--background-color: var(--color-normal-item-active);
}
}
</style>

View File

@@ -9,7 +9,7 @@
:disabled="isDisabled"
:required
class="select"
v-bind="attrs"
v-bind="$attrs"
>
<slot />
</select>
@@ -26,7 +26,7 @@
:disabled="isDisabled"
:required
class="textarea"
v-bind="attrs"
v-bind="$attrs"
/>
<input
v-else
@@ -37,7 +37,7 @@
:disabled="isDisabled"
:required
class="input"
v-bind="attrs"
v-bind="$attrs"
/>
<span v-if="before !== undefined" class="before">
<template v-if="typeof before === 'string'">{{ before }}</template>
@@ -59,7 +59,7 @@ import { IK_INPUT_ID, IK_INPUT_TYPE } from '@/types/injection-keys'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { useTextareaAutosize, useVModel } from '@vueuse/core'
import { computed, type HTMLAttributes, inject, nextTick, ref, useAttrs, watch } from 'vue'
import { computed, type HTMLAttributes, inject, nextTick, ref, watch } from 'vue'
defineOptions({ inheritAttrs: false })
@@ -84,8 +84,6 @@ const emit = defineEmits<{
'update:modelValue': [value: any]
}>()
const attrs = useAttrs()
const { name: contextColor } = useContext(ColorContext, () => props.color)
const inputElement = ref()
@@ -201,15 +199,15 @@ defineExpose({
&:not(:disabled) {
&.info {
&:hover {
--border-color: var(--color-info-item-hover);
--border-color: var(--color-normal-item-hover);
}
&:active {
--border-color: var(--color-info-item-active);
--border-color: var(--color-normal-item-active);
}
&:focus {
--border-color: var(--color-info-item-base);
--border-color: var(--color-normal-item-base);
}
}

View File

@@ -42,7 +42,7 @@ import { IK_FORM_HAS_LABEL, IK_INPUT_ID } from '@/types/injection-keys'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
import { uniqueId } from 'lodash-es'
import { computed, provide } from 'vue'
import { computed, provide, useSlots } from 'vue'
const props = withDefaults(
defineProps<{
@@ -59,6 +59,8 @@ const props = withDefaults(
{ disabled: undefined }
)
const slots = useSlots()
const id = computed(() => props.id ?? uniqueId('form-input-'))
provide(IK_INPUT_ID, id)
@@ -76,7 +78,7 @@ const color = computed<Color | undefined>(() => {
provide(
IK_FORM_HAS_LABEL,
computed(() => props.label !== undefined)
computed(() => slots.label !== undefined)
)
useContext(ColorContext, color)
@@ -126,7 +128,7 @@ useContext(DisabledContext, () => props.disabled)
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
& > span {
text-decoration: underline;

View File

@@ -53,7 +53,7 @@ whenever(
<style lang="postcss" scoped>
.collapsible {
padding: 1rem 1.5rem;
background-color: var(--color-info-background-selected);
background-color: var(--color-normal-background-selected);
border-radius: 0.8rem;
}
@@ -67,9 +67,9 @@ whenever(
display: flex;
align-items: center;
justify-content: space-between;
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
border: none;
border-bottom: 1px solid var(--color-info-txt-base);
border-bottom: 1px solid var(--color-normal-txt-base);
width: 100%;
padding-bottom: 1rem;
@@ -85,6 +85,6 @@ whenever(
}
.collapse-icon {
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
}
</style>

View File

@@ -1,27 +0,0 @@
<template>
<UiHeadBar>
<template #icon>
<UiObjectIcon size="medium" type="host" :state="getHostPowerState as HostState" />
</template>
{{ host.name_label }}
</UiHeadBar>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useHostStore } from '@/stores/xen-api/host.store'
import type { HostState } from '@core/types/object-icon.type'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import UiObjectIcon from '@core/components/ui/object-icon/UiObjectIcon.vue'
import { computed } from 'vue'
const props = defineProps<{
host: XenApiHost
}>()
const { runningHosts } = useHostStore().subscribe()
const getHostPowerState = computed(() => {
const isHostRunning = runningHosts.value.some(runningHost => runningHost.uuid === props.host.uuid)
return isHostRunning ? 'running' : 'halted'
})
</script>

View File

@@ -1,28 +0,0 @@
<template>
<TabList>
<RouterTab :to="{ name: 'host.dashboard', params: { uuid } }" disabled>
{{ $t('dashboard') }}
</RouterTab>
<RouterTab :to="{ name: 'host.console', params: { uuid } }">
{{ $t('console') }}
</RouterTab>
<RouterTab :to="{ name: 'host.network', params: { uuid } }" disabled>
{{ $t('network') }}
</RouterTab>
<RouterTab :to="{ name: 'host.tasks', params: { uuid } }" disabled>
{{ $t('tasks') }}
</RouterTab>
<RouterTab :to="{ name: 'host.vms', params: { uuid } }" disabled>
{{ $t('vms') }}
</RouterTab>
</TabList>
</template>
<script lang="ts" setup>
import RouterTab from '@/components/RouterTab.vue'
import TabList from '@core/components/tab/TabList.vue'
defineProps<{
uuid: string
}>()
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="infra-action">
<slot>
<UiIcon :icon fixed-width />
</slot>
</div>
</template>
<script lang="ts" setup>
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
defineProps<{
icon?: IconDefinition
}>()
</script>
<style lang="postcss" scoped>
.infra-action {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
width: 3rem;
height: 3rem;
}
</style>

View File

@@ -1,25 +1,25 @@
<template>
<VtsTreeItem v-if="host !== undefined" :expanded="isExpanded" class="infra-host-item">
<UiTreeItemLabel :icon="faServer" :route="{ name: 'host.console', params: { uuid: host.uuid } }" @toggle="toggle()">
<TreeItem v-if="host !== undefined" :expanded="isExpanded" class="infra-host-item">
<TreeItemLabel :icon="faServer" :route="{ name: 'host.dashboard', params: { uuid: host.uuid } }" @toggle="toggle()">
{{ host.name_label || '(Host)' }}
<template #addons>
<UiIcon v-if="isPoolMaster" v-tooltip="$t('master')" :icon="faStar" accent="warning" />
<UiCounter
<VtsIcon v-if="isPoolMaster" v-tooltip="$t('master')" :icon="faStar" accent="warning" />
<VtsCounter
v-if="isReady"
v-tooltip="$t('running-vm', { count: vmCount })"
:value="vmCount"
accent="info"
accent="brand"
variant="secondary"
size="small"
/>
</template>
</UiTreeItemLabel>
</TreeItemLabel>
<template #sublist>
<VtsTreeList>
<TreeList>
<InfraVmItems :host-opaque-ref="hostOpaqueRef" />
</VtsTreeList>
</TreeList>
</template>
</VtsTreeItem>
</TreeItem>
</template>
<script lang="ts" setup>
@@ -28,11 +28,11 @@ import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import UiIcon from '@core/components/icon/VtsIcon.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import VtsCounter from '@core/components/counter/VtsCounter.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import TreeItem from '@core/components/tree/TreeItem.vue'
import TreeItemLabel from '@core/components/tree/TreeItemLabel.vue'
import TreeList from '@core/components/tree/TreeList.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { faServer, faStar } from '@fortawesome/free-solid-svg-icons'
import { useToggle } from '@vueuse/shared'

View File

@@ -1,16 +1,16 @@
<template>
<VtsTreeItemError v-if="hasError">
<TreeItemError v-if="hasError">
{{ $t('error-no-data') }}
</VtsTreeItemError>
<VtsTreeLoadingItem v-else-if="!isReady" :icon="faServer">{{ $t('loading-hosts') }}</VtsTreeLoadingItem>
</TreeItemError>
<TreeLoadingItem v-else-if="!isReady" :icon="faServer">{{ $t('loading-hosts') }}</TreeLoadingItem>
<InfraHostItem v-for="host in hosts" :key="host.$ref" :host-opaque-ref="host.$ref" />
</template>
<script lang="ts" setup>
import InfraHostItem from '@/components/infra/InfraHostItem.vue'
import { useHostStore } from '@/stores/xen-api/host.store'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import TreeItemError from '@core/components/tree/TreeItemError.vue'
import TreeLoadingItem from '@core/components/tree/TreeLoadingItem.vue'
import { faServer } from '@fortawesome/free-solid-svg-icons'
const { records: hosts, isReady, hasError } = useHostStore().subscribe()

View File

@@ -0,0 +1,84 @@
<template>
<RouterLink v-slot="{ isExactActive, href, navigate }" :to="route" custom>
<div
:class="isExactActive ? 'exact-active' : $props.active ? 'active' : undefined"
class="infra-item-label"
v-bind="$attrs"
>
<a v-tooltip="{ selector: '.text-ellipsis' }" :href class="link" @click="navigate">
<UiIcon :icon class="icon" />
<div class="text-ellipsis typo h6-medium">
<slot />
</div>
</a>
<div class="actions">
<slot name="actions" />
</div>
</div>
</RouterLink>
</template>
<script lang="ts" setup>
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import type { RouteLocationRaw } from 'vue-router'
defineOptions({
inheritAttrs: false,
})
defineProps<{
icon: IconDefinition
route: RouteLocationRaw
active?: boolean
}>()
</script>
<style lang="postcss" scoped>
.infra-item-label {
display: flex;
align-items: stretch;
color: var(--color-neutral-txt-primary);
border-radius: 0.8rem;
background-color: var(--color-neutral-background-primary);
&:hover {
color: var(--color-neutral-txt-primary);
background-color: var(--color-neutral-background-secondary);
}
&:active,
&.active {
color: var(--color-normal-txt-base);
background-color: var(--color-neutral-background-primary);
}
&.exact-active {
color: var(--color-neutral-txt-primary);
background-color: var(--color-normal-background-selected);
.icon {
color: var(--color-normal-txt-base);
}
}
}
.link {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
padding: 1rem;
text-decoration: none;
color: inherit;
gap: 1rem;
font-weight: 500;
font-size: 2rem;
}
.actions {
display: flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<li class="infra-loading-item">
<div class="infra-item-label-placeholder">
<div class="link-placeholder">
<UiIcon :icon class="icon" />
<div class="loader">&nbsp;</div>
</div>
</div>
</li>
</template>
<script lang="ts" setup>
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
defineProps<{
icon: IconDefinition
}>()
</script>
<style lang="postcss" scoped>
.infra-item-label-placeholder {
display: flex;
height: 6rem;
margin-bottom: 0.2rem;
background-color: var(--color-neutral-background-primary);
}
.icon {
color: var(--color-neutral-txt-primary);
}
.link-placeholder {
display: flex;
align-items: center;
flex: 1;
padding: 0 1.5rem;
gap: 1rem;
}
.loader {
flex: 1;
animation: pulse alternate 1s infinite;
background-color: var(--color-normal-background-selected);
}
@keyframes pulse {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
</style>

View File

@@ -1,37 +1,37 @@
<template>
<VtsTreeList class="infra-pool-list">
<VtsTreeItem :expanded="isExpanded">
<VtsTreeItemError v-if="hasError">
<TreeList class="infra-pool-list">
<TreeItem :expanded="isExpanded">
<TreeItemError v-if="hasError">
{{ $t('error-no-data') }}
</VtsTreeItemError>
<VtsTreeLoadingItem v-else-if="!isReady || pool === undefined" :icon="faCity" />
<UiTreeItemLabel
</TreeItemError>
<TreeLoadingItem v-else-if="!isReady || pool === undefined" :icon="faCity" />
<TreeItemLabel
v-else
:icon="faCity"
:route="{ name: 'pool.dashboard', params: { uuid: pool.uuid } }"
@toggle="toggle()"
>
{{ pool.name_label || '(Pool)' }}
</UiTreeItemLabel>
</TreeItemLabel>
<template #sublist>
<VtsTreeList>
<TreeList>
<InfraHostItems />
<InfraVmItems />
</VtsTreeList>
</TreeList>
</template>
</VtsTreeItem>
</VtsTreeList>
</TreeItem>
</TreeList>
</template>
<script lang="ts" setup>
import InfraHostItems from '@/components/infra/InfraHostItems.vue'
import InfraVmItems from '@/components/infra/InfraVmItems.vue'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'
import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import TreeItem from '@core/components/tree/TreeItem.vue'
import TreeItemError from '@core/components/tree/TreeItemError.vue'
import TreeItemLabel from '@core/components/tree/TreeItemLabel.vue'
import TreeList from '@core/components/tree/TreeList.vue'
import TreeLoadingItem from '@core/components/tree/TreeLoadingItem.vue'
import { faCity } from '@fortawesome/free-solid-svg-icons'
import { useToggle } from '@vueuse/shared'

View File

@@ -1,21 +1,21 @@
<template>
<VtsTreeItem v-if="vm !== undefined" ref="rootElement" expanded class="infra-vm-item">
<UiTreeItemLabel v-if="isVisible" :route="{ name: 'vm.console', params: { uuid: vm.uuid } }" no-indent>
<TreeItem v-if="vm !== undefined" ref="rootElement" expanded class="infra-vm-item">
<TreeItemLabel v-if="isVisible" :route="{ name: 'vm.console', params: { uuid: vm.uuid } }" no-indent>
{{ vm.name_label || '(VM)' }}
<template #icon>
<UiObjectIcon size="medium" :state="vmPowerState!" type="vm" />
<ObjectIcon :state="vmPowerState!" type="vm" />
</template>
</UiTreeItemLabel>
</VtsTreeItem>
</TreeItemLabel>
</TreeItem>
</template>
<script lang="ts" setup>
import type { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import UiObjectIcon from '@core/components/ui/object-icon/UiObjectIcon.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import ObjectIcon from '@core/components/icon/ObjectIcon.vue'
import TreeItem from '@core/components/tree/TreeItem.vue'
import TreeItemLabel from '@core/components/tree/TreeItemLabel.vue'
import { useIntersectionObserver } from '@vueuse/core'
import { computed, ref } from 'vue'

View File

@@ -1,7 +1,7 @@
<template>
<VtsTreeItemError v-if="hasError">{{ $t('error-no-data') }}</VtsTreeItemError>
<TreeItemError v-if="hasError">{{ $t('error-no-data') }}</TreeItemError>
<template v-else-if="!isReady">
<VtsTreeLoadingItem v-for="i in 3" :key="i" :icon="faDisplay" />
<TreeLoadingItem v-for="i in 3" :key="i" :icon="faDisplay" />
</template>
<InfraVmItem v-for="vm in vms" :key="vm.$ref" :vm-opaque-ref="vm.$ref" />
</template>
@@ -10,8 +10,8 @@
import InfraVmItem from '@/components/infra/InfraVmItem.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import TreeItemError from '@core/components/tree/TreeItemError.vue'
import TreeLoadingItem from '@core/components/tree/TreeLoadingItem.vue'
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'

View File

@@ -23,7 +23,7 @@
</template>
<template #buttons>
<UiButton size="medium" accent="info" variant="tertiary" @click="addNewFilter()">
<UiButton size="medium" color="normal" level="tertiary" @click="addNewFilter()">
{{ $t('add-or') }}
</UiButton>
<ModalDeclineButton />
@@ -45,7 +45,7 @@ import UiBadge from '@/components/ui/UiBadge.vue'
import { getFilterIcon } from '@/libs/utils'
import type { Filters, NewFilter } from '@/types/filter'
import { IK_MODAL } from '@/types/injection-keys'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiButton from '@core/components/button/UiButton.vue'
import { Or, parse } from 'complex-matcher'
import { computed, inject, onMounted, ref } from 'vue'

View File

@@ -6,7 +6,7 @@
</template>
<template #buttons>
<UiButton size="medium" accent="info" variant="tertiary" @click="formatJson()">
<UiButton size="medium" color="normal" level="tertiary" @click="formatJson()">
{{ $t('reformat') }}
</UiButton>
<ModalDeclineButton />
@@ -25,7 +25,7 @@ import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import { IK_MODAL } from '@/types/injection-keys'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiButton from '@core/components/button/UiButton.vue'
import { faCode } from '@fortawesome/free-solid-svg-icons'
import { computed, inject, ref } from 'vue'

View File

@@ -3,7 +3,7 @@
<UiCardTitle>
{{ $t('alarms') }}
<template v-if="isReady && alarms.length > 0" #right>
<UiCounter :value="alarms.length" accent="danger" variant="primary" size="small" />
<VtsCounter :value="alarms.length" accent="danger" variant="primary" size="small" />
</template>
</UiCardTitle>
<div v-if="!isStarted" class="pre-start">
@@ -11,7 +11,7 @@
<p class="text typo h4-medium">
{{ $t('click-to-display-alarms') }}
</p>
<UiButton size="medium" accent="info" variant="primary" @click="start">{{ $t('load-now') }}</UiButton>
<UiButton size="medium" color="normal" level="primary" @click="start">{{ $t('load-now') }}</UiButton>
</div>
<div>
<img alt="" src="@/assets/server-status.svg" />
@@ -45,8 +45,8 @@ import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import UiTable from '@/components/ui/UiTable.vue'
import { useAlarmStore } from '@/stores/xen-api/alarm.store'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiButton from '@core/components/button/UiButton.vue'
import VtsCounter from '@core/components/counter/VtsCounter.vue'
const { records: alarms, start, isStarted, isReady, hasError } = useAlarmStore().subscribe({ defer: true })
</script>

View File

@@ -85,7 +85,7 @@ const hasError = computed(() => hostStoreHasError.value || vmStoreHasError.value
.progress-item {
margin-top: 2.6rem;
--progress-bar-height: 1.2rem;
--progress-bar-color: var(--color-info-item-base);
--progress-bar-color: var(--color-normal-item-base);
--progress-bar-background-color: var(--color-neutral-background-disabled);
&.warning {

View File

@@ -1,5 +1,5 @@
<template>
<UiCard class="pool-dashboard-hosts-patches">
<UiCard>
<UiCardTitle class="patches-title">
{{ $t('patches') }}
<template v-if="areAllLoaded && count > 0" #right>
@@ -30,10 +30,6 @@ const { count, patches, areSomeLoaded, areAllLoaded } = useHostPatches(hosts)
</script>
<style lang="postcss" scoped>
.pool-dashboard-hosts-patches {
min-width: 43.8rem;
}
.patches-title {
--section-title-right-color: var(--color-danger-txt-base);
}

View File

@@ -1,5 +1,5 @@
<template>
<UiCard class="pool-dashboard-status" :color="hasError ? 'error' : undefined">
<UiCard :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t('status') }}</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="!isReady" />
@@ -54,9 +54,3 @@ const totalVmsCount = computed(() => vms.value.length)
const activeVmsCount = computed(() => vms.value.filter(vm => vm.power_state === 'Running').length)
</script>
<style lang="postcss" scoped>
.pool-dashboard-status {
min-width: 32rem;
}
</style>

View File

@@ -69,7 +69,7 @@ td[colspan='5'] {
}
.loader {
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
display: block;
font-size: 4rem;
margin: 2rem auto 0;

View File

@@ -62,8 +62,8 @@ const isDisabled = useContext(DisabledContext, () => props.disabled)
&:active,
&.active,
&.busy {
color: var(--color-info-txt-base);
background-color: var(--color-info-background-selected);
color: var(--color-normal-txt-base);
background-color: var(--color-normal-background-selected);
}
}
}

View File

@@ -17,7 +17,7 @@ import UiSpinner from '@/components/ui/UiSpinner.vue'
}
.spinner {
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
font-size: 4rem;
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<div class="ui-section-title" :class="tags.left">
<component :is="tags.left" v-if="slots.default || left" class="left">
<component :is="tags.left" v-if="$slots.default || left" class="left">
<slot>{{ left }}</slot>
<UiCounter v-if="count > 0" class="count" :value="count" accent="info" variant="primary" size="small" />
<VtsCounter v-if="count > 0" class="count" :value="count" accent="brand" variant="primary" size="small" />
</component>
<component :is="tags.right" v-if="slots.right || right" class="right">
<component :is="tags.right" v-if="$slots.right || right" class="right">
<slot name="right">{{ right }}</slot>
</component>
</div>
@@ -12,7 +12,7 @@
<script lang="ts" setup>
import { UiCardTitleLevel } from '@/types/enums'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import VtsCounter from '@core/components/counter/VtsCounter.vue'
import { computed } from 'vue'
const props = withDefaults(
@@ -25,11 +25,6 @@ const props = withDefaults(
{ count: 0, level: UiCardTitleLevel.Title }
)
const slots = defineSlots<{
default?(): any
right?(): any
}>()
const tags = computed(() => {
switch (props.level) {
case UiCardTitleLevel.Subtitle:
@@ -52,7 +47,7 @@ const tags = computed(() => {
--section-title-left-color: var(--color-neutral-txt-primary);
--section-title-left-weight: 500;
--section-title-right-size: 1.6rem;
--section-title-right-color: var(--color-info-txt-base);
--section-title-right-color: var(--color-normal-txt-base);
--section-title-right-weight: 700;
&.h6 {
@@ -65,12 +60,12 @@ const tags = computed(() => {
&.h5 {
margin-top: 2rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-info-txt-base);
border-bottom: 1px solid var(--color-normal-txt-base);
--section-title-left-size: 1.6rem;
--section-title-left-color: var(--color-info-txt-base);
--section-title-left-color: var(--color-normal-txt-base);
--section-title-left-weight: 700;
--section-title-right-size: 1.4rem;
--section-title-right-color: var(--color-info-txt-base);
--section-title-right-color: var(--color-normal-txt-base);
--section-title-right-weight: 400;
}
}

View File

@@ -26,11 +26,11 @@ const emit = defineEmits<{
align-items: stretch;
justify-content: center;
height: 3.4rem;
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
border-radius: 1.7rem;
background-color: var(--color-info-background-selected);
background-color: var(--color-normal-background-selected);
gap: 1rem;
border: 1px solid var(--color-info-txt-base);
border: 1px solid var(--color-normal-txt-base);
}
.label,
@@ -54,10 +54,10 @@ const emit = defineEmits<{
border-radius: 1.4rem;
width: 2.8rem;
margin: 0.2rem;
background-color: var(--color-info-item-active);
background-color: var(--color-normal-item-active);
&:hover {
background-color: var(--color-danger-item-hover);
background-color: var(----color-danger-item-hover);
}
}
</style>

View File

@@ -1,21 +1,14 @@
<template>
<tr class="ui-key-value-row">
<th v-if="slots.key" class="key typo p1-regular">
<th v-if="$slots.key" class="key typo p1-regular">
<slot name="key" />
</th>
<td :colspan="slots.key ? 1 : 2" class="value typo p1-regular">
<td :colspan="$slots.key ? 1 : 2" class="value typo p1-regular">
<slot name="value" />
</td>
</tr>
</template>
<script lang="ts" setup>
const slots = defineSlots<{
value(): any
key?(): any
}>()
</script>
<style lang="postcss" scoped>
.key,
.value {

View File

@@ -24,7 +24,7 @@ defineProps<{
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
}
.title {

View File

@@ -33,7 +33,7 @@ defineProps<{
:deep(thead) {
th,
td {
color: var(--color-info-txt-base);
color: var(--color-normal-txt-base);
font-size: 1.4rem;
font-weight: 400;
text-transform: uppercase;

View File

@@ -38,7 +38,7 @@ const icon = computed(() => {
}
&.info {
--icon-color: var(--color-info-item-base);
--icon-color: var(--color-normal-item-base);
}
&.success {

View File

@@ -1,12 +1,12 @@
<template>
<UiButton size="medium" accent="info" variant="primary" :busy="modal.isBusy" type="submit">
<UiButton size="medium" color="normal" level="primary" :busy="modal.isBusy" type="submit">
<slot />
</UiButton>
</template>
<script lang="ts" setup>
import { IK_MODAL } from '@/types/injection-keys'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiButton from '@core/components/button/UiButton.vue'
import { inject } from 'vue'
const modal = inject(IK_MODAL)!

Some files were not shown because too many files have changed in this diff Show More