mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-15 03:20:48 -06:00
Compare commits
1 Commits
web-stack/
...
log-trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
175c689933 |
@@ -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',
|
||||
|
||||
3
@vates/fatfs/.gitignore
vendored
3
@vates/fatfs/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
.DS_Store
|
||||
.vscode/
|
||||
node_modules/
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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); }
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
});
|
||||
@@ -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); }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -19,12 +19,12 @@
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^4.3.0",
|
||||
"@xen-orchestra/fs": "^4.2.1",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"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"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^4.7.0"
|
||||
"xen-api": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^18.7.0",
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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.3",
|
||||
"@xen-orchestra/fs": "^4.2.1",
|
||||
"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",
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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))
|
||||
@@ -191,7 +191,6 @@ export class ImportVmBackup {
|
||||
version: '1.0.0',
|
||||
vifs,
|
||||
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
|
||||
vtpms,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.57.0",
|
||||
"version": "0.54.3",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
},
|
||||
@@ -28,7 +28,7 @@
|
||||
"@vates/nbd-client": "^3.1.2",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^4.3.0",
|
||||
"@xen-orchestra/fs": "^4.2.1",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"app-conf": "^3.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.5.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.7.1"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.5.1",
|
||||
"xen-api": "^4.7.0"
|
||||
"xen-api": "^4.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "4.3.0",
|
||||
"version": "4.2.1",
|
||||
"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",
|
||||
|
||||
@@ -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/backups": "^0.54.3",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"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",
|
||||
|
||||
@@ -2,15 +2,8 @@
|
||||
|
||||
## **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)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/lite",
|
||||
"version": "0.6.0",
|
||||
"version": "0.5.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",
|
||||
@@ -32,7 +31,7 @@
|
||||
"@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.4.0",
|
||||
"complex-matcher": "^0.7.1",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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" />
|
||||
@@ -70,12 +70,6 @@ useUnreachableHosts()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -38,12 +38,12 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore)
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 5.6rem;
|
||||
padding: 0 1.6rem;
|
||||
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 {
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,53 +1,61 @@
|
||||
<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">
|
||||
<MenuList placement="bottom-end" border>
|
||||
<template #trigger="{ open, isOpen }">
|
||||
<UiAccountMenuButton
|
||||
v-tooltip="isOpen ? false : { content: $t('settings'), placement: 'left' }"
|
||||
:selected="isOpen"
|
||||
size="medium"
|
||||
@click="open($event)"
|
||||
/>
|
||||
</template>
|
||||
<MenuItem :icon="faGear" @click="openSettings">{{ $t('settings') }}</MenuItem>
|
||||
<MenuItem :icon="faBook" @click="openUrl('https://docs.xcp-ng.org?utm_campaign=xolite&utm_term=xcpdoc')">
|
||||
{{ $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">
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:icon="faHeadset"
|
||||
@click="openUrl('https://vates.tech/pricing-and-support?utm_campaign=xolite&utm_term=pricing')"
|
||||
>
|
||||
{{ $t('professional-support') }}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:icon="faComments"
|
||||
@click="openUrl('https://xcp-ng.org/forum/topic/4731/xen-orchestra-lite?utm_campaign=xolite&utm_term=forum')"
|
||||
>
|
||||
{{ $t('access-forum') }}
|
||||
</MenuItem>
|
||||
<MenuItem :icon="faArrowRightFromBracket" class="menu-item-logout" @click="logout">
|
||||
{{ $t('log-out') }}
|
||||
</VtsMenuItem>
|
||||
</VtsMenuList>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</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 MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import MenuList from '@core/components/menu/MenuList.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 { nextTick } from 'vue'
|
||||
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' })
|
||||
}),
|
||||
},
|
||||
})
|
||||
const logout = () => {
|
||||
const xenApiStore = useXenApiStore()
|
||||
xenApiStore.disconnect()
|
||||
nextTick(() => router.push({ name: 'home' }))
|
||||
}
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url, '_blank', 'noopener noreferrer')
|
||||
}
|
||||
const openSettings = () => router.push({ name: 'settings' })
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
:deep(.menu-item-logout) {
|
||||
.menu-item-logout {
|
||||
color: var(--color-danger-txt-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,23 +7,23 @@
|
||||
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">
|
||||
@@ -43,8 +43,8 @@
|
||||
<template #right>
|
||||
<UiButton
|
||||
v-if="eventsLog.length > 0"
|
||||
accent="info"
|
||||
size="medium"
|
||||
accent="info"
|
||||
variant="tertiary"
|
||||
@click="eventsLog = []"
|
||||
>
|
||||
@@ -91,13 +91,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 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 +266,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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,6 +1,10 @@
|
||||
<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()">
|
||||
<UiTreeItemLabel
|
||||
: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" />
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<VtsMenuItem :icon="faCopy" v-bind="menuItem">
|
||||
<MenuItem
|
||||
v-tooltip="!areAllSelectedVmsHalted && $t(isSingleAction ? 'vm-is-running' : 'selected-vms-in-execution')"
|
||||
:busy="areSomeSelectedVmsCloning"
|
||||
:disabled="isDisabled"
|
||||
:icon="faCopy"
|
||||
@click="handleCopy"
|
||||
>
|
||||
{{ $t('copy') }}
|
||||
</VtsMenuItem>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -10,42 +16,41 @@ import { VM_OPERATION, 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 { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import { type MenuLike, useMenuAction } from '@core/packages/menu'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import { faCopy } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
menu: MenuLike
|
||||
selectedRefs: XenApiVm['$ref'][]
|
||||
isSingleAction?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { getByOpaqueRef } = useVmStore().subscribe()
|
||||
|
||||
const { getByOpaqueRefs } = useVmStore().subscribe()
|
||||
|
||||
const selectedVms = computed(() => getByOpaqueRefs(props.selectedRefs))
|
||||
|
||||
const vmRefsToClone = computed(() =>
|
||||
Object.fromEntries(selectedVms.value.map(vm => [vm.$ref, `${vm.name_label} (COPY)`]))
|
||||
const selectedVms = computed(() =>
|
||||
props.selectedRefs.map(vmRef => getByOpaqueRef(vmRef)).filter((vm): vm is XenApiVm => vm !== undefined)
|
||||
)
|
||||
|
||||
const menuItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
handler: () => useXenApiStore().getXapi().vm.clone(vmRefsToClone.value),
|
||||
disabled: () => {
|
||||
if (props.selectedRefs.length === 0) {
|
||||
return true
|
||||
}
|
||||
const areAllSelectedVmsHalted = computed(
|
||||
() =>
|
||||
selectedVms.value.length > 0 &&
|
||||
selectedVms.value.every(selectedVm => selectedVm.power_state === VM_POWER_STATE.HALTED)
|
||||
)
|
||||
|
||||
if (!selectedVms.value.every(selectedVm => selectedVm.power_state === VM_POWER_STATE.HALTED)) {
|
||||
return props.isSingleAction ? t('vm-is-running') : t('selected-vms-in-execution')
|
||||
}
|
||||
const areSomeSelectedVmsCloning = computed(() =>
|
||||
selectedVms.value.some(vm => isVmOperationPending(vm, VM_OPERATION.CLONE))
|
||||
)
|
||||
|
||||
return false
|
||||
},
|
||||
busy: computed(() => selectedVms.value.some(vm => isVmOperationPending(vm, VM_OPERATION.CLONE))),
|
||||
const isDisabled = computed(() => {
|
||||
return selectedVms.value.length === 0 || !areAllSelectedVmsHalted.value
|
||||
})
|
||||
|
||||
const handleCopy = async () => {
|
||||
const xapiStore = useXenApiStore()
|
||||
|
||||
const vmRefsToClone = Object.fromEntries(selectedVms.value.map(vm => [vm.$ref, `${vm.name_label} (COPY)`]))
|
||||
|
||||
await xapiStore.getXapi().vm.clone(vmRefsToClone)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<VtsMenuItem :icon="faTrashCan" v-bind="menuItem">
|
||||
<MenuItem
|
||||
v-tooltip="areSomeVmsInExecution && $t('selected-vms-in-execution')"
|
||||
:disabled="isDisabled"
|
||||
:icon="faTrashCan"
|
||||
@click="openDeleteModal"
|
||||
>
|
||||
{{ $t('delete') }}
|
||||
</VtsMenuItem>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -9,39 +14,25 @@ import { useModal } from '@/composables/modal.composable'
|
||||
import { 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 VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import { type MenuLike, useMenuAction } from '@core/packages/menu'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import { faTrashCan } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
menu: MenuLike
|
||||
vmRefs: XenApiVm['$ref'][]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { getByOpaqueRef: getVm } = useVmStore().subscribe()
|
||||
|
||||
const { getByOpaqueRefs: getVms } = useVmStore().subscribe()
|
||||
const vms = computed<XenApiVm[]>(() => props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined))
|
||||
|
||||
const vms = computed(() => getVms(props.vmRefs))
|
||||
const areSomeVmsInExecution = computed(() => vms.value.some(vm => vm.power_state !== VM_POWER_STATE.HALTED))
|
||||
|
||||
const menuItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
handler: () =>
|
||||
useModal(() => import('@/components/modals/VmDeleteModal.vue'), {
|
||||
vmRefs: props.vmRefs,
|
||||
}),
|
||||
disabled: computed(() => {
|
||||
if (vms.value.length === 0) {
|
||||
return true
|
||||
}
|
||||
const isDisabled = computed(() => vms.value.length === 0 || areSomeVmsInExecution.value)
|
||||
|
||||
if (vms.value.some(vm => vm.power_state !== VM_POWER_STATE.HALTED)) {
|
||||
return t('selected-vms-in-execution')
|
||||
}
|
||||
|
||||
return false
|
||||
}),
|
||||
})
|
||||
const openDeleteModal = () =>
|
||||
useModal(() => import('@/components/modals/VmDeleteModal.vue'), {
|
||||
vmRefs: props.vmRefs,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
<template>
|
||||
<VtsMenuItem :icon="faDisplay" v-bind="menuItem">
|
||||
<MenuItem
|
||||
v-tooltip="
|
||||
vmRefs.length > 0 && !isSomeExportable && $t(isSingleAction ? 'vm-is-running' : 'no-selected-vm-can-be-exported')
|
||||
"
|
||||
:disabled="isDisabled"
|
||||
:icon="faDisplay"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ $t(isSingleAction ? 'export-vm' : 'export-vms') }}
|
||||
</VtsMenuItem>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useContext } from '@/composables/context.composable'
|
||||
import { useModal } from '@/composables/modal.composable'
|
||||
import { DisabledContext } from '@/context'
|
||||
import { areSomeVmOperationAllowed } from '@/libs/vm'
|
||||
import { VM_OPERATION } 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 VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import { type MenuLike, useMenuAction } from '@core/packages/menu'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
menu: MenuLike
|
||||
vmRefs: XenApiVm['$ref'][]
|
||||
isSingleAction?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getByOpaqueRefs } = useVmStore().subscribe()
|
||||
|
||||
const vms = computed(() => getByOpaqueRefs(props.vmRefs))
|
||||
const isParentDisabled = useContext(DisabledContext)
|
||||
|
||||
const menuItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
handler: () =>
|
||||
useModal(() => import('@/components/modals/VmExportModal.vue'), {
|
||||
vmRefs: props.vmRefs,
|
||||
}),
|
||||
disabled: () => {
|
||||
if (props.vmRefs.length === 0) {
|
||||
return true
|
||||
}
|
||||
const isSomeExportable = computed(() =>
|
||||
getByOpaqueRefs(props.vmRefs).some(vm => areSomeVmOperationAllowed(vm, VM_OPERATION.EXPORT))
|
||||
)
|
||||
|
||||
if (!vms.value.some(vm => areSomeVmOperationAllowed(vm, VM_OPERATION.EXPORT))) {
|
||||
return props.isSingleAction ? t('vm-is-running') : t('no-selected-vm-can-be-exported')
|
||||
}
|
||||
const isDisabled = computed(() => isParentDisabled.value || !isSomeExportable.value)
|
||||
|
||||
return false
|
||||
},
|
||||
})
|
||||
const openModal = () => {
|
||||
useModal(() => import('@/components/modals/VmExportModal.vue'), {
|
||||
vmRefs: props.vmRefs,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<template>
|
||||
<li>
|
||||
<VtsMenuTrigger :icon="faFileExport" v-bind="toggle.$trigger">
|
||||
{{ $t('export') }}
|
||||
</VtsMenuTrigger>
|
||||
|
||||
<VtsMenuList v-bind="toggle.$target">
|
||||
<VmActionExportItem :menu="toggle" :vm-refs="vmRefs" />
|
||||
<VtsMenuItem :icon="faCode" v-bind="toggle.exportAsJson">
|
||||
<MenuItem :icon="faFileExport">
|
||||
{{ $t('export') }}
|
||||
<template #submenu>
|
||||
<VmActionExportItem :vm-refs="vmRefs" />
|
||||
<MenuItem :icon="faCode" @click="exportVmsAsJsonFile(vms, `vms_${new Date().toISOString()}.json`)">
|
||||
{{ $t('export-table-to', { type: '.json' }) }}
|
||||
</VtsMenuItem>
|
||||
<VtsMenuItem :icon="faFileCsv" v-bind="toggle.exportAsCsv">
|
||||
</MenuItem>
|
||||
<MenuItem :icon="faFileCsv" @click="exportVmsAsCsvFile(vms, `vms_${new Date().toISOString()}.csv`)">
|
||||
{{ $t('export-table-to', { type: '.csv' }) }}
|
||||
</VtsMenuItem>
|
||||
</VtsMenuList>
|
||||
</li>
|
||||
</MenuItem>
|
||||
</template>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -21,31 +18,14 @@ import VmActionExportItem from '@/components/vm/VmActionItems/VmActionExportItem
|
||||
import { exportVmsAsCsvFile, exportVmsAsJsonFile } from '@/libs/vm'
|
||||
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
|
||||
import { useVmStore } from '@/stores/xen-api/vm.store'
|
||||
import VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import VtsMenuList from '@core/components/menu/VtsMenuList.vue'
|
||||
import VtsMenuTrigger from '@core/components/menu/VtsMenuTrigger.vue'
|
||||
import { action, type MenuLike, useMenuToggle } from '@core/packages/menu'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import { faCode, faFileCsv, faFileExport } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
menu: MenuLike
|
||||
vmRefs: XenApiVm['$ref'][]
|
||||
}>()
|
||||
|
||||
const { getByOpaqueRefs } = useVmStore().subscribe()
|
||||
|
||||
const vms = computed(() => getByOpaqueRefs(props.vmRefs))
|
||||
|
||||
const toggle = useMenuToggle({
|
||||
parent: props.menu,
|
||||
items: {
|
||||
exportAsJson: action(() => exportVmsAsJsonFile(vms.value, `vms_${new Date().toISOString()}.json`), {
|
||||
disabled: computed(() => vms.value.length === 0),
|
||||
}),
|
||||
exportAsCsv: action(() => exportVmsAsCsvFile(vms.value, `vms_${new Date().toISOString()}.csv`), {
|
||||
disabled: computed(() => vms.value.length === 0),
|
||||
}),
|
||||
},
|
||||
})
|
||||
const { getByOpaqueRef: getVm } = useVmStore().subscribe()
|
||||
const vms = computed(() => props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined))
|
||||
</script>
|
||||
|
||||
@@ -1,50 +1,55 @@
|
||||
<template>
|
||||
<VtsMenuItem :icon="faRoute" v-bind="menuItem">
|
||||
<MenuItem
|
||||
v-tooltip="
|
||||
selectedRefs.length > 0 &&
|
||||
!isMigratable &&
|
||||
$t(isSingleAction ? 'this-vm-cant-be-migrated' : 'no-selected-vm-can-be-migrated')
|
||||
"
|
||||
:busy="isMigrating"
|
||||
:disabled="isParentDisabled || !isMigratable"
|
||||
:icon="faRoute"
|
||||
@click="openModal()"
|
||||
>
|
||||
{{ $t('migrate') }}
|
||||
</VtsMenuItem>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useContext } from '@/composables/context.composable'
|
||||
import { useModal } from '@/composables/modal.composable'
|
||||
import { DisabledContext } from '@/context'
|
||||
import { areSomeVmOperationAllowed, isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION } 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 VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import { type MenuLike, useMenuAction } from '@core/packages/menu'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import { faRoute } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
menu: MenuLike
|
||||
selectedRefs: XenApiVm['$ref'][]
|
||||
isSingleAction?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getByOpaqueRefs } = useVmStore().subscribe()
|
||||
|
||||
const vms = computed(() => getByOpaqueRefs(props.selectedRefs))
|
||||
const isParentDisabled = useContext(DisabledContext)
|
||||
|
||||
const menuItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
handler: () =>
|
||||
useModal(() => import('@/components/modals/VmMigrateModal.vue'), {
|
||||
vmRefs: props.selectedRefs,
|
||||
}),
|
||||
disabled: () => {
|
||||
if (props.selectedRefs.length === 0) {
|
||||
return true
|
||||
}
|
||||
const isMigratable = computed(() =>
|
||||
getByOpaqueRefs(props.selectedRefs).some(vm =>
|
||||
areSomeVmOperationAllowed(vm, [VM_OPERATION.POOL_MIGRATE, VM_OPERATION.MIGRATE_SEND])
|
||||
)
|
||||
)
|
||||
|
||||
if (!vms.value.some(vm => areSomeVmOperationAllowed(vm, [VM_OPERATION.POOL_MIGRATE, VM_OPERATION.MIGRATE_SEND]))) {
|
||||
return props.isSingleAction ? t('this-vm-cant-be-migrated') : t('no-selected-vm-can-be-migrated')
|
||||
}
|
||||
},
|
||||
busy: computed(() =>
|
||||
vms.value.some(vm => isVmOperationPending(vm, [VM_OPERATION.POOL_MIGRATE, VM_OPERATION.MIGRATE_SEND]))
|
||||
),
|
||||
})
|
||||
const isMigrating = computed(() =>
|
||||
getByOpaqueRefs(props.selectedRefs).some(vm =>
|
||||
isVmOperationPending(vm, [VM_OPERATION.POOL_MIGRATE, VM_OPERATION.MIGRATE_SEND])
|
||||
)
|
||||
)
|
||||
|
||||
const openModal = () =>
|
||||
useModal(() => import('@/components/modals/VmMigrateModal.vue'), {
|
||||
vmRefs: props.selectedRefs,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<VtsMenuItem :icon="faPlay" v-bind="startItem">
|
||||
<MenuItem :busy="areVmsBusyToStart" :disabled="!areVmsHalted" :icon="faPlay" @click="xenApi.vm.start(vmRefs)">
|
||||
{{ $t('start') }}
|
||||
</VtsMenuItem>
|
||||
<li>
|
||||
<VtsMenuTrigger :icon="faServer" v-bind="startOnHostItem.$trigger">
|
||||
{{ $t('start-on-host') }}
|
||||
</VtsMenuTrigger>
|
||||
<VtsMenuList border v-bind="startOnHostItem.$target">
|
||||
<VtsMenuItem v-for="host in hosts" :key="host.$ref" :icon="faServer" v-bind="startOnHostItem[host.$ref]">
|
||||
</MenuItem>
|
||||
<MenuItem :busy="areVmsBusyToStartOnHost" :disabled="!areVmsHalted" :icon="faServer">
|
||||
{{ $t('start-on-host') }}
|
||||
<template #submenu>
|
||||
<MenuItem v-for="host in hosts" :key="host.$ref" :icon="faServer" @click="xenApi.vm.startOn(vmRefs, host.$ref)">
|
||||
<div class="wrapper">
|
||||
{{ host.name_label }}
|
||||
<div>
|
||||
@@ -15,30 +13,55 @@
|
||||
<PowerStateIcon :state="getHostState(host)" />
|
||||
</div>
|
||||
</div>
|
||||
</VtsMenuItem>
|
||||
</VtsMenuList>
|
||||
</li>
|
||||
<VtsMenuItem :icon="faPause" v-bind="pauseItem">
|
||||
</MenuItem>
|
||||
</template>
|
||||
</MenuItem>
|
||||
<MenuItem :busy="areVmsBusyToPause" :disabled="!areVmsRunning" :icon="faPause" @click="xenApi.vm.pause(vmRefs)">
|
||||
{{ $t('pause') }}
|
||||
</VtsMenuItem>
|
||||
<VtsMenuItem :icon="faMoon" v-bind="suspendItem">
|
||||
</MenuItem>
|
||||
<MenuItem :busy="areVmsBusyToSuspend" :disabled="!areVmsRunning" :icon="faMoon" @click="xenApi.vm.suspend(vmRefs)">
|
||||
{{ $t('suspend') }}
|
||||
</VtsMenuItem>
|
||||
<VtsMenuItem :icon="faCirclePlay" v-bind="resumeItem">
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:busy="areVmsBusyToResume"
|
||||
:disabled="!areVmsSuspended && !areVmsPaused"
|
||||
:icon="faCirclePlay"
|
||||
@click="xenApi.vm.resume(vmRefsWithPowerState)"
|
||||
>
|
||||
{{ $t('resume') }}
|
||||
</VtsMenuItem>
|
||||
<VtsMenuItem :icon="faRotateLeft" v-bind="rebootItem">
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:busy="areVmsBusyToReboot"
|
||||
:disabled="!areVmsRunning"
|
||||
:icon="faRotateLeft"
|
||||
@click="xenApi.vm.reboot(vmRefs)"
|
||||
>
|
||||
{{ $t('reboot') }}
|
||||
</VtsMenuItem>
|
||||
<VtsMenuItem :icon="faRepeat" v-bind="forceRebootItem">
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:busy="areVmsBusyToForceReboot"
|
||||
:disabled="!areVmsRunning && !areVmsPaused"
|
||||
:icon="faRepeat"
|
||||
@click="xenApi.vm.reboot(vmRefs, true)"
|
||||
>
|
||||
{{ $t('force-reboot') }}
|
||||
</VtsMenuItem>
|
||||
<VtsMenuItem :icon="faPowerOff" v-bind="shutdownItem">
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:busy="areVmsBusyToShutdown"
|
||||
:disabled="!areVmsRunning"
|
||||
:icon="faPowerOff"
|
||||
@click="xenApi.vm.shutdown(vmRefs)"
|
||||
>
|
||||
{{ $t('shutdown') }}
|
||||
</VtsMenuItem>
|
||||
<VtsMenuItem :icon="faPlug" v-bind="forceShutdownItem">
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:busy="areVmsBusyToForceShutdown"
|
||||
:disabled="!areVmsRunning && !areVmsSuspended && !areVmsPaused"
|
||||
:icon="faPlug"
|
||||
@click="xenApi.vm.shutdown(vmRefs, true)"
|
||||
>
|
||||
{{ $t('force-shutdown') }}
|
||||
</VtsMenuItem>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -52,10 +75,7 @@ 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 { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import VtsMenuList from '@core/components/menu/VtsMenuList.vue'
|
||||
import VtsMenuTrigger from '@core/components/menu/VtsMenuTrigger.vue'
|
||||
import { action, type MenuLike, useMenuAction, useMenuToggle } from '@core/packages/menu'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import {
|
||||
faCirclePlay,
|
||||
faMoon,
|
||||
@@ -71,26 +91,17 @@ import {
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
menu: MenuLike
|
||||
vmRefs: XenApiVm['$ref'][]
|
||||
}>()
|
||||
|
||||
const { getByOpaqueRefs } = useVmStore().subscribe()
|
||||
const { getByOpaqueRef: getVm } = useVmStore().subscribe()
|
||||
const { records: hosts } = useHostStore().subscribe()
|
||||
const { pool } = usePoolStore().subscribe()
|
||||
const { isHostRunning } = useHostMetricsStore().subscribe()
|
||||
|
||||
const vms = computed(() => getByOpaqueRefs(props.vmRefs))
|
||||
const vms = computed(() => props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined))
|
||||
|
||||
const vmRefsWithPowerState = computed(() =>
|
||||
vms.value.reduce(
|
||||
(acc, vm) => ({
|
||||
...acc,
|
||||
[vm.$ref]: vm.power_state,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
)
|
||||
const vmRefsWithPowerState = computed(() => vms.value.reduce((acc, vm) => ({ ...acc, [vm.$ref]: vm.power_state }), {}))
|
||||
|
||||
const xenApi = useXenApiStore().getXapi()
|
||||
|
||||
@@ -112,78 +123,6 @@ const areVmsBusyToForceReboot = computed(() => areOperationsPending(VM_OPERATION
|
||||
const areVmsBusyToShutdown = computed(() => areOperationsPending(VM_OPERATION.CLEAN_SHUTDOWN))
|
||||
const areVmsBusyToForceShutdown = computed(() => areOperationsPending(VM_OPERATION.HARD_SHUTDOWN))
|
||||
const getHostState = (host: XenApiHost) => (isHostRunning(host) ? VM_POWER_STATE.RUNNING : VM_POWER_STATE.HALTED)
|
||||
|
||||
const hasVm = computed(() => vms.value.length > 0)
|
||||
|
||||
const startItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToStart,
|
||||
disabled: computed(() => !hasVm.value || !areVmsHalted.value),
|
||||
handler: () => xenApi.vm.start(props.vmRefs),
|
||||
})
|
||||
|
||||
const startOnHostItem = useMenuToggle({
|
||||
parent: props.menu,
|
||||
placement: 'right-start',
|
||||
items: Object.fromEntries(
|
||||
hosts.value.map(host => [
|
||||
host.$ref,
|
||||
action(() => xenApi.vm.startOn(props.vmRefs, host.$ref), {
|
||||
busy: areVmsBusyToStartOnHost,
|
||||
disabled: computed(() => !hasVm.value || !areVmsHalted.value),
|
||||
}),
|
||||
])
|
||||
),
|
||||
})
|
||||
|
||||
const pauseItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToPause,
|
||||
disabled: computed(() => !hasVm.value || !areVmsRunning.value),
|
||||
handler: () => xenApi.vm.pause(props.vmRefs),
|
||||
})
|
||||
|
||||
const suspendItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToSuspend,
|
||||
disabled: computed(() => !hasVm.value || !areVmsRunning.value),
|
||||
handler: () => xenApi.vm.suspend(props.vmRefs),
|
||||
})
|
||||
|
||||
const resumeItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToResume,
|
||||
disabled: computed(() => !hasVm.value || (!areVmsSuspended.value && !areVmsPaused.value)),
|
||||
handler: () => xenApi.vm.resume(vmRefsWithPowerState.value),
|
||||
})
|
||||
|
||||
const rebootItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToReboot,
|
||||
disabled: computed(() => !hasVm.value || !areVmsRunning.value),
|
||||
handler: () => xenApi.vm.reboot(props.vmRefs),
|
||||
})
|
||||
|
||||
const forceRebootItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToForceReboot,
|
||||
disabled: computed(() => !hasVm.value || (!areVmsRunning.value && !areVmsPaused.value)),
|
||||
handler: () => xenApi.vm.reboot(props.vmRefs, true),
|
||||
})
|
||||
|
||||
const shutdownItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToShutdown,
|
||||
disabled: computed(() => !hasVm.value || !areVmsRunning.value),
|
||||
handler: () => xenApi.vm.shutdown(props.vmRefs),
|
||||
})
|
||||
|
||||
const forceShutdownItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
busy: areVmsBusyToForceShutdown,
|
||||
disabled: computed(() => !hasVm.value || (!areVmsRunning.value && !areVmsSuspended.value && !areVmsPaused.value)),
|
||||
handler: () => xenApi.vm.shutdown(props.vmRefs, true),
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<VtsMenuItem :icon="faCamera" v-bind="menuItem">
|
||||
<MenuItem :busy="areSomeVmsSnapshoting" :disabled="isDisabled" :icon="faCamera" @click="handleSnapshot">
|
||||
{{ $t('snapshot') }}
|
||||
</VtsMenuItem>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -10,28 +10,28 @@ import { VM_OPERATION } 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 { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import { type MenuLike, useMenuAction } from '@core/packages/menu'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import { faCamera } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
menu: MenuLike
|
||||
vmRefs: XenApiVm['$ref'][]
|
||||
}>()
|
||||
|
||||
const { getByOpaqueRefs } = useVmStore().subscribe()
|
||||
const { getByOpaqueRef } = useVmStore().subscribe()
|
||||
|
||||
const vms = computed(() => getByOpaqueRefs(props.vmRefs))
|
||||
|
||||
const vmRefsToSnapshot = computed(() =>
|
||||
Object.fromEntries(vms.value.map(vm => [vm.$ref, `${vm.name_label}_${new Date().toISOString()}`]))
|
||||
const vms = computed(() =>
|
||||
props.vmRefs.map(vmRef => getByOpaqueRef(vmRef)).filter((vm): vm is XenApiVm => vm !== undefined)
|
||||
)
|
||||
|
||||
const menuItem = useMenuAction({
|
||||
parent: props.menu,
|
||||
handler: () => useXenApiStore().getXapi().vm.snapshot(vmRefsToSnapshot.value),
|
||||
busy: computed(() => vms.value.some(vm => isVmOperationPending(vm, VM_OPERATION.SNAPSHOT))),
|
||||
disabled: computed(() => vms.value.length === 0),
|
||||
})
|
||||
const areSomeVmsSnapshoting = computed(() => vms.value.some(vm => isVmOperationPending(vm, VM_OPERATION.SNAPSHOT)))
|
||||
|
||||
const isDisabled = computed(() => vms.value.length === 0 || areSomeVmsSnapshoting.value)
|
||||
|
||||
const handleSnapshot = () => {
|
||||
const vmRefsToSnapshot = Object.fromEntries(
|
||||
vms.value.map(vm => [vm.$ref, `${vm.name_label}_${new Date().toISOString()}`])
|
||||
)
|
||||
return useXenApiStore().getXapi().vm.snapshot(vmRefsToSnapshot)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,50 @@
|
||||
<template>
|
||||
<TitleBar :icon="faDisplay">
|
||||
{{ name }}
|
||||
<template v-if="vm !== undefined" #actions>
|
||||
<UiDropdownButton :icon="faPowerOff" v-bind="omit(powerMenu.$trigger, ['as', 'submenu'])">
|
||||
{{ $t('change-state') }}
|
||||
</UiDropdownButton>
|
||||
|
||||
<VtsMenuList border v-bind="powerMenu.$target">
|
||||
<VmActionPowerStateItems :menu="powerMenu" :vm-refs="[vm.$ref]" />
|
||||
</VtsMenuList>
|
||||
|
||||
<UiButtonIcon
|
||||
v-tooltip="{
|
||||
placement: 'left',
|
||||
content: $t('more-actions'),
|
||||
}"
|
||||
:icon="faEllipsisVertical"
|
||||
accent="info"
|
||||
class="more-actions-button"
|
||||
size="large"
|
||||
v-bind="omit(moreMenu.$trigger, ['as', 'submenu'])"
|
||||
/>
|
||||
|
||||
<VtsMenuList border v-bind="moreMenu.$target">
|
||||
<VmActionCopyItem :menu="moreMenu" :selected-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionExportItem :menu="moreMenu" :vm-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionSnapshotItem :menu="moreMenu" :vm-refs="[vm.$ref]" />
|
||||
<VmActionMigrateItem :menu="moreMenu" :selected-refs="[vm.$ref]" is-single-action />
|
||||
</VtsMenuList>
|
||||
<template #actions>
|
||||
<MenuList v-if="vm !== undefined" placement="bottom-end" border>
|
||||
<template #trigger="{ open, isOpen }">
|
||||
<UiButton
|
||||
size="medium"
|
||||
accent="info"
|
||||
variant="primary"
|
||||
:class="{ pressed: isOpen }"
|
||||
:left-icon="faPowerOff"
|
||||
@click="open"
|
||||
>
|
||||
{{ $t('change-state') }}
|
||||
<UiIcon :icon="faAngleDown" />
|
||||
</UiButton>
|
||||
</template>
|
||||
<VmActionPowerStateItems :vm-refs="[vm.$ref]" />
|
||||
</MenuList>
|
||||
<MenuList v-if="vm !== undefined" placement="bottom-end" border>
|
||||
<template #trigger="{ open, isOpen }">
|
||||
<UiButtonIcon
|
||||
v-tooltip="{
|
||||
placement: 'left',
|
||||
content: $t('more-actions'),
|
||||
}"
|
||||
:selected="isOpen"
|
||||
:icon="faEllipsisVertical"
|
||||
accent="info"
|
||||
class="more-actions-button"
|
||||
size="large"
|
||||
@click="open"
|
||||
/>
|
||||
</template>
|
||||
<VmActionCopyItem :selected-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionExportItem :vm-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionSnapshotItem :vm-refs="[vm.$ref]" />
|
||||
<VmActionMigrateItem :selected-refs="[vm.$ref]" is-single-action />
|
||||
</MenuList>
|
||||
</template>
|
||||
</TitleBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TitleBar from '@/components/TitleBar.vue'
|
||||
import UiIcon from '@/components/ui/icon/UiIcon.vue'
|
||||
import VmActionCopyItem from '@/components/vm/VmActionItems/VmActionCopyItem.vue'
|
||||
import VmActionExportItem from '@/components/vm/VmActionItems/VmActionExportItem.vue'
|
||||
import VmActionMigrateItem from '@/components/vm/VmActionItems/VmActionMigrateItem.vue'
|
||||
@@ -41,13 +52,11 @@ import VmActionPowerStateItems from '@/components/vm/VmActionItems/VmActionPower
|
||||
import VmActionSnapshotItem from '@/components/vm/VmActionItems/VmActionSnapshotItem.vue'
|
||||
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
|
||||
import { useVmStore } from '@/stores/xen-api/vm.store'
|
||||
import VtsMenuList from '@core/components/menu/VtsMenuList.vue'
|
||||
import MenuList from '@core/components/menu/MenuList.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiDropdownButton from '@core/components/ui/dropdown-button/UiDropdownButton.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import { useMenuToggle } from '@core/packages/menu'
|
||||
import { faDisplay, faEllipsisVertical, faPowerOff } from '@fortawesome/free-solid-svg-icons'
|
||||
import { objectOmit as omit } from '@vueuse/shared'
|
||||
import { faAngleDown, faDisplay, faEllipsisVertical, faPowerOff } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -57,8 +66,4 @@ const { currentRoute } = useRouter()
|
||||
const vm = computed(() => getVmByUuid(currentRoute.value.params.uuid as XenApiVm['uuid']))
|
||||
|
||||
const name = computed(() => vm.value?.name_label)
|
||||
|
||||
const powerMenu = useMenuToggle({ placement: 'bottom-end' })
|
||||
|
||||
const moreMenu = useMenuToggle({ placement: 'bottom-end' })
|
||||
</script>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<template>
|
||||
<UiButtonIcon v-if="isMobile" :icon="faEllipsis" accent="info" size="medium" v-bind="menu.$trigger" />
|
||||
|
||||
<VtsMenuList
|
||||
:border="isMobile"
|
||||
<MenuList
|
||||
:disabled="selectedRefs.length === 0"
|
||||
:horizontal="!isMobile"
|
||||
:border="isMobile"
|
||||
class="vms-actions-bar"
|
||||
v-bind="isMobile ? menu.$target : undefined"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<VtsMenuItem :icon="faPowerOff" v-bind="menu.powerState.$trigger">
|
||||
<template v-if="isMobile" #trigger="{ isOpen, open }">
|
||||
<UiButtonIcon accent="info" size="medium" :selected="isOpen" :icon="faEllipsis" @click="open" />
|
||||
</template>
|
||||
<MenuItem :icon="faPowerOff">
|
||||
{{ $t('change-state') }}
|
||||
</VtsMenuItem>
|
||||
<Teleport to="body">
|
||||
<VtsMenuList border v-bind="menu.powerState.$target">
|
||||
<VmActionPowerStateItems :menu="menu.powerState" :vm-refs="selectedRefs" />
|
||||
</VtsMenuList>
|
||||
</Teleport>
|
||||
<VmActionMigrateItem :menu :selected-refs="selectedRefs" />
|
||||
<VmActionCopyItem :menu :selected-refs="selectedRefs" />
|
||||
<VtsMenuItem :icon="faEdit" v-bind="menu.editConfig">
|
||||
<template #submenu>
|
||||
<VmActionPowerStateItems :vm-refs="selectedRefs" />
|
||||
</template>
|
||||
</MenuItem>
|
||||
<VmActionMigrateItem :selected-refs="selectedRefs" />
|
||||
<VmActionCopyItem :selected-refs="selectedRefs" />
|
||||
<MenuItem v-tooltip="$t('coming-soon')" :icon="faEdit">
|
||||
{{ $t('edit-config') }}
|
||||
</VtsMenuItem>
|
||||
<VmActionSnapshotItem :menu :vm-refs="selectedRefs" />
|
||||
<VmActionExportItems :menu :vm-refs="selectedRefs" />
|
||||
<VmActionDeleteItem :menu :vm-refs="selectedRefs" />
|
||||
</VtsMenuList>
|
||||
</MenuItem>
|
||||
<VmActionSnapshotItem :vm-refs="selectedRefs" />
|
||||
<VmActionExportItems :vm-refs="selectedRefs" />
|
||||
<VmActionDeleteItem :vm-refs="selectedRefs" />
|
||||
</MenuList>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -34,32 +34,20 @@ import VmActionMigrateItem from '@/components/vm/VmActionItems/VmActionMigrateIt
|
||||
import VmActionPowerStateItems from '@/components/vm/VmActionItems/VmActionPowerStateItems.vue'
|
||||
import VmActionSnapshotItem from '@/components/vm/VmActionItems/VmActionSnapshotItem.vue'
|
||||
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
|
||||
import VtsMenuItem from '@core/components/menu/VtsMenuItem.vue'
|
||||
import VtsMenuList from '@core/components/menu/VtsMenuList.vue'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import MenuList from '@core/components/menu/MenuList.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import { action, toggle, useMenuToggle } from '@core/packages/menu'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import { useUiStore } from '@core/stores/ui.store'
|
||||
import { faEdit, faEllipsis, faPowerOff } from '@fortawesome/free-solid-svg-icons'
|
||||
import { noop } from '@vueuse/shared'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
selectedRefs: XenApiVm['$ref'][]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isMobile } = storeToRefs(useUiStore())
|
||||
|
||||
const menu = useMenuToggle({
|
||||
placement: 'bottom-end',
|
||||
items: {
|
||||
powerState: toggle({}),
|
||||
editConfig: action(noop, { disabled: t('coming-soon') }),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { XenApiHost, XenApiVm } from '@/libs/xen-api/xen-api.types'
|
||||
import { defineJobArg } from '@core/packages/job'
|
||||
|
||||
export const vmsArg = defineJobArg({
|
||||
identify: (vm: XenApiVm) => vm.$ref,
|
||||
toArray: true,
|
||||
})
|
||||
|
||||
export const hostArg = defineJobArg({
|
||||
identify: (host: XenApiHost) => host.$ref,
|
||||
toArray: false,
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmCopyJob = defineJob('vm.copy', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => {
|
||||
const vmRefsToClone = Object.fromEntries(vms.map(vm => [vm.$ref, `${vm.name_label} (COPY)`]))
|
||||
|
||||
return xapi.vm.clone(vmRefsToClone)
|
||||
},
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-copy.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.CLONE))) {
|
||||
throw new JobRunningError(t('job.vm-copy.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.HALTED)) {
|
||||
throw new JobError(t('job.vm-copy.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmDeleteJob = defineJob('vm.delete', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => xapi.vm.delete(vms.map(vm => vm.$ref)),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-delete.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.DESTROY))) {
|
||||
throw new JobRunningError(t('job.vm-delete.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.HALTED)) {
|
||||
throw new JobError(t('job.vm-delete.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { areSomeVmOperationAllowed, isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_COMPRESSION_TYPE, VM_OPERATION } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, defineJobArg, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const compressionArg = defineJobArg<VM_COMPRESSION_TYPE>({
|
||||
identify: false,
|
||||
toArray: false,
|
||||
})
|
||||
|
||||
export const useVmExportJob = defineJob('vm.export', [vmsArg, compressionArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: (vms, compression) =>
|
||||
xapi.vm.export(
|
||||
vms.map(vm => vm.$ref),
|
||||
compression
|
||||
),
|
||||
validate: (isRunning, vms, compression) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-export.missing-vms'))
|
||||
}
|
||||
|
||||
if (!compression) {
|
||||
throw new JobError(t('job.vm-export.missing-compression'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.EXPORT))) {
|
||||
throw new JobRunningError(t('job.vm-export.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => areSomeVmOperationAllowed(vm, VM_OPERATION.EXPORT))) {
|
||||
throw new JobError(t('job.vm-export.not-allowed'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmForceRebootJob = defineJob('vm.force-reboot', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms =>
|
||||
xapi.vm.reboot(
|
||||
vms.map(vm => vm.$ref),
|
||||
true
|
||||
),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-force-reboot.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.HARD_REBOOT))) {
|
||||
throw new JobRunningError(t('job.vm-force-reboot.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.RUNNING || vm.power_state === VM_POWER_STATE.PAUSED)) {
|
||||
throw new JobError(t('job.vm-force-reboot.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmForceShutdownJob = defineJob('vm.force-shutdown', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms =>
|
||||
xapi.vm.shutdown(
|
||||
vms.map(vm => vm.$ref),
|
||||
true
|
||||
),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-force-shutdown.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.HARD_SHUTDOWN))) {
|
||||
throw new JobRunningError(t('job.vm-force-shutdown.in-progress'))
|
||||
}
|
||||
|
||||
if (
|
||||
!vms.every(
|
||||
vm =>
|
||||
vm.power_state === VM_POWER_STATE.RUNNING ||
|
||||
vm.power_state === VM_POWER_STATE.SUSPENDED ||
|
||||
vm.power_state === VM_POWER_STATE.PAUSED
|
||||
)
|
||||
) {
|
||||
throw new JobError(t('job.vm-force-shutdown.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,43 +0,0 @@
|
||||
import { hostArg, vmsArg } from '@/jobs/args'
|
||||
import { areSomeVmOperationAllowed, isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmMigrateJob = defineJob('vm.migrate', [vmsArg, hostArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: (vms, host) =>
|
||||
xapi.vm.migrate(
|
||||
vms.map(vm => vm.$ref),
|
||||
host.$ref
|
||||
),
|
||||
validate: (isRunning, vms, host) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-migrate.missing-vms'))
|
||||
}
|
||||
|
||||
if (!host) {
|
||||
throw new JobError(t('job.vm-migrate.missing-host'))
|
||||
}
|
||||
|
||||
if (
|
||||
isRunning ||
|
||||
vms.some(vm => isVmOperationPending(vm, [VM_OPERATION.POOL_MIGRATE, VM_OPERATION.MIGRATE_SEND]))
|
||||
) {
|
||||
throw new JobRunningError(t('job.vm-migrate.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.RUNNING)) {
|
||||
throw new JobError(t('job.vm-migrate.bad-power-state'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => areSomeVmOperationAllowed(vm, [VM_OPERATION.POOL_MIGRATE, VM_OPERATION.MIGRATE_SEND]))) {
|
||||
throw new JobError(t('job.vm-export.not-allowed'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmPauseJob = defineJob('vm.pause', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: async vms => xapi.vm.pause(vms.map(vm => vm.$ref)),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-pause.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.PAUSE))) {
|
||||
throw new JobRunningError(t('job.vm-pause.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.RUNNING)) {
|
||||
throw new JobError(t('job.vm-pause.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmRebootJob = defineJob('vm.reboot', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => xapi.vm.reboot(vms.map(vm => vm.$ref)),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-reboot.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.CLEAN_REBOOT))) {
|
||||
throw new JobRunningError(t('job.vm-reboot.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.RUNNING || vm.power_state === VM_POWER_STATE.PAUSED)) {
|
||||
throw new JobError(t('job.vm-reboot.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmResumeJob = defineJob('vm.resume', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => xapi.vm.resume(Object.fromEntries(vms.map(vm => [vm.$ref, vm.power_state]))),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-resume.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, [VM_OPERATION.UNPAUSE, VM_OPERATION.RESUME]))) {
|
||||
throw new JobRunningError(t('job.vm-resume.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.PAUSED || vm.power_state === VM_POWER_STATE.SUSPENDED)) {
|
||||
throw new JobError(t('job.vm-resume.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmShutdownJob = defineJob('vm.shutdown', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => xapi.vm.shutdown(vms.map(vm => vm.$ref)),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-shutdown.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.CLEAN_SHUTDOWN))) {
|
||||
throw new JobRunningError(t('job.vm-shutdown.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.RUNNING)) {
|
||||
throw new JobError(t('job.vm-shutdown.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmSnapshotJob = defineJob('vm.snapshot', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => {
|
||||
const vmRefsToSnapshot = Object.fromEntries(
|
||||
vms.map(vm => [vm.$ref, `${vm.name_label}_${new Date().toISOString()}`])
|
||||
)
|
||||
|
||||
return xapi.vm.snapshot(vmRefsToSnapshot)
|
||||
},
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-snapshot.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.SNAPSHOT))) {
|
||||
throw new JobRunningError(t('job.vm-snapshot.in-progress'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
import { hostArg, vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmStartOnJob = defineJob('vm.start-on', [vmsArg, hostArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: (vms, host) =>
|
||||
xapi.vm.startOn(
|
||||
vms.map(vm => vm.$ref),
|
||||
host.$ref
|
||||
),
|
||||
validate: (isRunning, vms, host) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-start-on.missing-vms'))
|
||||
}
|
||||
|
||||
if (!host) {
|
||||
throw new JobError(t('job.vm-start-on.missing-host'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.START_ON))) {
|
||||
throw new JobRunningError(t('job.vm-start-on.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.HALTED)) {
|
||||
throw new JobError(t('job.vm-start-on.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmStartJob = defineJob('vm.start', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => xapi.vm.start(vms.map(vm => vm.$ref)),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-start.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.START))) {
|
||||
throw new JobRunningError(t('job.vm-start.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.HALTED)) {
|
||||
throw new JobError(t('job.vm-start.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { vmsArg } from '@/jobs/args'
|
||||
import { isVmOperationPending } from '@/libs/vm'
|
||||
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useVmSuspendJob = defineJob('vm.suspend', [vmsArg], () => {
|
||||
const xapi = useXenApiStore().getXapi()
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
run: vms => xapi.vm.suspend(vms.map(vm => vm.$ref)),
|
||||
validate: (isRunning, vms) => {
|
||||
if (vms.length === 0) {
|
||||
throw new JobError(t('job.vm-suspend.missing-vms'))
|
||||
}
|
||||
|
||||
if (isRunning || vms.some(vm => isVmOperationPending(vm, VM_OPERATION.SUSPEND))) {
|
||||
throw new JobRunningError(t('job.vm-suspend.in-progress'))
|
||||
}
|
||||
|
||||
if (!vms.every(vm => vm.power_state === VM_POWER_STATE.RUNNING)) {
|
||||
throw new JobError(t('job.vm-suspend.bad-power-state'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums'
|
||||
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
|
||||
import { useXenApiStore } from '@/stores/xen-api.store'
|
||||
import type { XenApiPatch } from '@/types/xen-api'
|
||||
import { castArray } from 'lodash-es'
|
||||
|
||||
export async function fetchMissingHostPatches(hostRef: XenApiHost['$ref']): Promise<XenApiPatch[]> {
|
||||
const xenApiStore = useXenApiStore()
|
||||
@@ -18,9 +16,3 @@ export async function fetchMissingHostPatches(hostRef: XenApiHost['$ref']): Prom
|
||||
$id: `${rawPatch.name}-${rawPatch.version}`,
|
||||
}))
|
||||
}
|
||||
|
||||
export const isHostOperationPending = (host: XenApiHost, operations: HOST_OPERATION[] | HOST_OPERATION) => {
|
||||
const currentOperations = Object.values(host.current_operations)
|
||||
|
||||
return castArray(operations).some(operation => currentOperations.includes(operation))
|
||||
}
|
||||
|
||||
@@ -90,10 +90,6 @@ export enum ON_NORMAL_EXIT {
|
||||
RESTART = 'restart',
|
||||
}
|
||||
|
||||
export enum HOST_OPERATION {
|
||||
SHUTDOWN = 'shutdown',
|
||||
}
|
||||
|
||||
export enum VM_OPERATION {
|
||||
ASSERT_OPERATION_VALID = 'assert_operation_valid',
|
||||
AWAITING_MEMORY_LIVE = 'awaiting_memory_live',
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
ALLOCATION_ALGORITHM,
|
||||
BOND_MODE,
|
||||
DOMAIN_TYPE,
|
||||
HOST_OPERATION,
|
||||
IP_CONFIGURATION_MODE,
|
||||
IPV6_CONFIGURATION_MODE,
|
||||
NETWORK_DEFAULT_LOCKING_MODE,
|
||||
@@ -112,7 +111,6 @@ export interface XenApiHost extends XenApiRecord<'host'> {
|
||||
cpu_info: { cpu_count: string }
|
||||
software_version: { product_version: string }
|
||||
control_domain: XenApiVm['$ref']
|
||||
current_operations: Record<string, HOST_OPERATION>
|
||||
}
|
||||
|
||||
export interface XenApiSr extends XenApiRecord<'sr'> {
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
{
|
||||
"about": "O aplikaci",
|
||||
"access-xoa": "Přistoupit do XOA",
|
||||
"add": "Přidat",
|
||||
"add-filter": "Přidat filtr",
|
||||
"add-or": "+NEBO",
|
||||
"add-sort": "Přidat řazení",
|
||||
"admin-login": "Přihlašovací jméno správce",
|
||||
"admin-password": "Heslo správce",
|
||||
"admin-password-confirm": "Potvrzení hesla správce",
|
||||
|
||||
"alarm-type.cpu_usage": "Vytížení procesoru překračuje {n}%",
|
||||
"alarm-type.disk_usage": "Využití disku překračuje {n}%",
|
||||
"alarm-type.fs_usage": "Využití souborového systému překračuje {n}%",
|
||||
"alarm-type.log_fs_usage": "Využití souborového systému, uchovávájícího záznamy událostí překračuje {n}%",
|
||||
"alarm-type.mem_usage": "Využití operační paměti překračuje {n}%",
|
||||
"alarm-type.memory_free_kib": "Volná operační paměť padá pod {n}%",
|
||||
"alarm-type.network_usage": "Vytížení sítě překračuje {n}%",
|
||||
"alarm-type.physical_utilisation": "Fyzické využití překračuje {n}%",
|
||||
"alarm-type.sr_io_throughput_total_per_host": "Celkový průtok přes SR IO na hostiteli přesahuje {n}%",
|
||||
"alarm-type.unknown": "Neznámý typ výstrahy",
|
||||
|
||||
"all-good": "Všechno v pořádku!",
|
||||
"allow-self-signed-ssl": "Je možné že bude třeba ve vámi využívaném prohlížeči třeba povolit samy sebou podepsané SSL certifikáty",
|
||||
"appearance": "Vzhled",
|
||||
"ascending": "vzestupně",
|
||||
"available-properties-for-advanced-filter": "Vlastnosti pro pokročilý filtr, které jsou k dispozici:",
|
||||
"back-pool-dashboard": "Přejít zpět na přehled vašeho fondu",
|
||||
"backup": "Záloha",
|
||||
"cancel": "Storno",
|
||||
"change-state": "Změnit stav",
|
||||
"check-errors": "Zkontrolujte chyby:",
|
||||
"click-to-display-alarms": "Kliknutím zobrazíte výstrahy:",
|
||||
"click-to-return-default-pool": "Kliknutím sem se vrátíte na výchozí fond",
|
||||
"close": "Zavřít",
|
||||
"community": "Komunita",
|
||||
"community-name": "Komunita kolem {name}",
|
||||
"configuration": "Nastavení",
|
||||
"confirm-cancel": "Opravdu chcete zrušit?",
|
||||
"confirm-delete": "Chystáte se smazat {0}",
|
||||
"console-unavailable": "Konzole není k dispozici",
|
||||
"copy": "Zkopírovat",
|
||||
"cpu-provisioning": "přednastavení procesoru",
|
||||
"cpu-provisioning-warning": "Počet přidělených virt. procesorů překračuje počet přítomných fyzických procesorů. Může mít dopad na výkon systému",
|
||||
"cpu-usage": "Využití procesoru",
|
||||
"delete": "Smazat",
|
||||
"delete-vms": "Smazat 1 virt. stroj | Smazat {n} virt. strojů",
|
||||
"deploy": "Nasadit",
|
||||
"deploy-xoa": "Nasadit XOA",
|
||||
"deploy-xoa-available-on-desktop": "Nasazení XOA je k dispozici na vašem desktopovém rozhraní",
|
||||
"deploy-xoa-custom-ntp-servers": "Uživatelsky určené NTP servery (oddělováno mezerou)",
|
||||
|
||||
"deploy-xoa-status.configuring": "Nastavování XOA…",
|
||||
"deploy-xoa-status.importing": "Importování XOA…",
|
||||
"deploy-xoa-status.not-responding": "XOA neodpovídá",
|
||||
"deploy-xoa-status.ready": "XOA je připraveno!",
|
||||
"deploy-xoa-status.starting": "Spouštění XOA…",
|
||||
"deploy-xoa-status.waiting": "Čeká se až XOA odpoví…",
|
||||
|
||||
"descending": "sestupně",
|
||||
"description": "Popis",
|
||||
"dhcp": "DHCP",
|
||||
"disabled": "Zakázáno",
|
||||
"display": "Zobrazení",
|
||||
"dns": "DNS",
|
||||
"do-you-have-needs": "Něco zde chybí a/nebo očekáváte? Dejte nám vědět",
|
||||
"documentation": "Dokumentace",
|
||||
"edit-config": "Upravit nastavení",
|
||||
"enabled": "Povoleno",
|
||||
"error-occurred": "Došlo k chybě",
|
||||
"export": "Export",
|
||||
"export-n-vms": "Exportovat 1 virt. stroj | Exportovat {n} virt. strojů",
|
||||
"export-n-vms-manually": "Exportovat 1 virt. stroj ručně | Exportovat {n} virt. strojů ručně",
|
||||
"export-table-to": "Exportovat tabulku do {type}",
|
||||
"export-vm": "Exportovat virt. stroj",
|
||||
"export-vms": "Exportovat virt. stroje",
|
||||
"export-vms-manually-information": "Některé exporty virt. strojů nebylo možné spustit automaticky – nejspíš kvůli nastavení vámi využívaného prohlížeče. Pokud je chcete exportovat, klikněte na každý z nich. (Případně zkopírujte také odkaz.)",
|
||||
"fetching-fresh-data": "Získávání aktuálních údajů",
|
||||
|
||||
"filter.comparison.contains": "Obsahuje",
|
||||
"filter.comparison.ends-with": "Končí na",
|
||||
"filter.comparison.equals": "Rovno",
|
||||
"filter.comparison.is": "Je",
|
||||
"filter.comparison.is-false": "Neplatí",
|
||||
"filter.comparison.is-not": "Není",
|
||||
"filter.comparison.is-true": "Platí",
|
||||
"filter.comparison.matches-regex": "Odpovídá regulárnímu výrazu",
|
||||
"filter.comparison.not-contain": "Neobsahuje",
|
||||
"filter.comparison.not-end-with": "Nekončí na",
|
||||
"filter.comparison.not-equal": "Není rovno",
|
||||
"filter.comparison.not-match-regex": "Neodpovídá regulárnímu výrazu",
|
||||
"filter.comparison.not-start-with": "Nezačíná na",
|
||||
"filter.comparison.starts-with": "Začíná na",
|
||||
|
||||
"following-hosts-unreachable": "Následující hostitelé nejsou dosažitelní",
|
||||
"force-reboot": "Vynutit restart",
|
||||
"force-shutdown": "Vynutit vypnutí",
|
||||
"fullscreen": "Na celou obrazovku",
|
||||
"fullscreen-leave": "Ukončit celoobrazovkový režim",
|
||||
"gateway": "Bránka",
|
||||
"n-gb-left": "{n} GB zbývá",
|
||||
"n-gb-required": "{n} GB vyžadováno",
|
||||
"go-back": "Přejít zpět",
|
||||
"gzip": "gzip",
|
||||
"here": "Zde",
|
||||
|
||||
"host.active": "Aktivní",
|
||||
"host.inactive": "Neaktivní",
|
||||
|
||||
"invalid-field": "Neplatný obsah kolonky",
|
||||
"keep-me-logged": "Neodhlašovat",
|
||||
"keep-page-open": "Panel prohlížeče nenačítejte znovu nebo ho neopouštějte dokud nasazení neskončí.",
|
||||
"language": "Jazyk",
|
||||
"last-week": "Minulý týden",
|
||||
"load-now": "Načíst nyní",
|
||||
"loading-hosts": "Načítání hostitelů…",
|
||||
"login": "Přihlášení",
|
||||
"login-only-on-master": "Přihlášení je možné pouze na hlavním hostiteli",
|
||||
"more-actions": "Další akce",
|
||||
"migrate": "Přestěhovat",
|
||||
"migrate-n-vms": "Přestěhovat 1 virt. stroj | Přestěhovat {n} virt. strojů",
|
||||
"n-hosts-awaiting-patch": "{n} hostitelů čeká na tuto záplatu | {n} hostitelů čeká na tuto záplatu",
|
||||
"n-missing": "{n} chybí",
|
||||
"name": "Název",
|
||||
"netmask": "Maska sítě",
|
||||
"network-download": "Stáhnout",
|
||||
"network-throughput": "Propustnost sítě",
|
||||
"network-upload": "Nahrát",
|
||||
"new-features-are-coming": "Nové funkce budou brzy následovat!",
|
||||
"news": "Novinky",
|
||||
"news-name": "Novinky v {name}",
|
||||
"no-alarm-triggered": "Nespuštěna žádná výstraha",
|
||||
"no-result": "Nic nenalezeno",
|
||||
"no-selected-vm-can-be-exported": "Žádný z označených virt. strojů není možné exportovat",
|
||||
"no-selected-vm-can-be-migrated": "Žádný z označených virt. strojů není možné přestěhovat",
|
||||
"no-tasks": "Žádné úlohy",
|
||||
"not-found": "Nenalezeno",
|
||||
"object": "Objekt",
|
||||
"ok": "OK",
|
||||
"on-object": "na {object}",
|
||||
"open-console-in-new-tab": "Otevřít konzoli v novém panelu",
|
||||
"or": "Nebo",
|
||||
"page-not-found": "Tato stránka není k nalezení…",
|
||||
"password": "Heslo",
|
||||
"password-invalid": "Neplatné heslo",
|
||||
"pause": "Pozastavit",
|
||||
"please-confirm": "Prosím potvrďte",
|
||||
"pool-cpu-usage": "Dotazovat využití procesoru",
|
||||
"pool-ram-usage": "Dotazovat využití operační paměti",
|
||||
"power-state": "Stav napájení",
|
||||
"property": "Vlastnost",
|
||||
"ram-usage": "Využití operační paměti",
|
||||
"reboot": "Restartovat",
|
||||
"reformat": "Přeformátovat",
|
||||
|
||||
"relative-time.day": "1 den | {n} dnů",
|
||||
"relative-time.future": "V {str}",
|
||||
"relative-time.hour": "1 hodina | {n} hodin",
|
||||
"relative-time.minute": "1 minuta | {n} minut",
|
||||
"relative-time.month": "1 měsíc | {n} měsíců",
|
||||
"relative-time.now": "Právě teď",
|
||||
"relative-time.past": "před {str}",
|
||||
"relative-time.second": "1 sekunda | {n} sekund",
|
||||
"relative-time.year": "1 rok | {n} let",
|
||||
|
||||
"resume": "Navázat v chodu",
|
||||
"save": "Uložit",
|
||||
"select-compression": "Vyberte kompresi",
|
||||
"select-destination-host": "Vyberte cílového hostitele",
|
||||
"selected-vms-in-execution": "Některé z vybraných virt. strojů jsou spuštěné",
|
||||
"send-ctrl-alt-del": "Odeslat Ctrl+Alt+Del",
|
||||
"send-us-feedback": "Poskytnout nám zpětnou vazbu",
|
||||
|
||||
"select.network": "Vyberte síť",
|
||||
"select.storage": "Vyberte úložiště",
|
||||
|
||||
"settings": "Nastavení",
|
||||
"shutdown": "Vypnout",
|
||||
"snapshot": "Zachycený stav",
|
||||
"sort-by": "Seřadit podle",
|
||||
"ssh-account": "SSH účet",
|
||||
"ssh-login": "SSH přihlašovací jméno",
|
||||
"ssh-password": "SSH heslo",
|
||||
"ssh-password-confirm": "Potvrďte zadání SSH hesla",
|
||||
"stacked-cpu-usage": "Celkem využití procesoru",
|
||||
"stacked-ram-usage": "Celkem využítí opearční paměti",
|
||||
"start": "Spustit",
|
||||
"start-on-host": "Spustit na konkrétním hostiteli",
|
||||
"static-ip": "Statická IP adresa",
|
||||
"status": "Stav",
|
||||
"storage-usage": "Využití úložiště",
|
||||
"support": "Podpora",
|
||||
"suspend": "Uspat",
|
||||
"switch-theme": "Přepnout motiv vzhledu",
|
||||
|
||||
"task.estimated-end": "Odhadovaný konec",
|
||||
"task.progress": "Postup",
|
||||
"task.started": "Zahájeno",
|
||||
|
||||
"theme-auto": "Automaticky",
|
||||
"theme-dark": "Tmavé",
|
||||
"theme-light": "Světelé",
|
||||
"this-vm-cant-be-migrated": "Tento virt. stroj není možné přestěhovat",
|
||||
"top-#": "Nej {n}",
|
||||
"total-free:": "Celkem volno:",
|
||||
"total-used:": "Celkem využito:",
|
||||
"unreachable-hosts": "Nedosažitelní hostitelé",
|
||||
"unreachable-hosts-reload-page": "Hotovo, načtěte stránku znovu",
|
||||
"vcpus": "virt. procesorů",
|
||||
"vcpus-used": "využito virt. procesorů",
|
||||
"version": "Verze",
|
||||
|
||||
"vm.active": "Aktivní",
|
||||
"vm.inactive": "Neaktivní",
|
||||
|
||||
"vm-is-running": "Virt. stroj je spuštěný",
|
||||
"xo-lite-under-construction": "XOLite teprve vzniká",
|
||||
"xoa-admin-account": "Účet správce v XOA",
|
||||
"xoa-deploy": "Nasazení XOA",
|
||||
"xoa-deploy-failed": "Je nám líto, ale nasazení se nezdařilo!",
|
||||
"xoa-deploy-retry": "Zkusit XOA nasadit znovu",
|
||||
"xoa-deploy-successful": "XOA úspěšně nasazeno!",
|
||||
"xoa-ip": "IP adresa pro XOA",
|
||||
"xoa-password-confirm-different": "Heslo do XOA nebylo zadáno dvakrát stejně",
|
||||
"xoa-ssh-account": "Účet pro SSH přístup do XOA",
|
||||
"xoa-ssh-password-confirm-different": "Heslo pro SSH nebylo zadáno dvakrát stejně",
|
||||
"you-are-currently-on": "V tuto chvíli provozujete na: {0}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
@@ -65,6 +65,7 @@
|
||||
"documentation": "Dokumentation",
|
||||
"edit-config": "Einstellungen anpassen",
|
||||
"enabled": "Aktiviert",
|
||||
"error-no-data": "Fehler beim Datenabruf.",
|
||||
"error-occurred": "Ein Fehler ist aufgetreten",
|
||||
"export": "Exportieren",
|
||||
"export-n-vms": "1 VM exportieren | {n} VMs exportieren",
|
||||
@@ -93,7 +94,9 @@
|
||||
"following-hosts-unreachable": "Die folgenden Hosts sind nicht erreichbar",
|
||||
"force-reboot": "Neustart erzwingen",
|
||||
"force-shutdown": "Herunterfahren erzwingen",
|
||||
"fullscreen": "Vollbild",
|
||||
"fullscreen-leave": "Vollbild schließen",
|
||||
"gateway": "Gateway",
|
||||
"n-gb-left": "{n} GB frei",
|
||||
"n-gb-required": "{n} GB benötigt",
|
||||
"go-back": "Zurück",
|
||||
@@ -134,7 +137,9 @@
|
||||
"object": "Objekt",
|
||||
"ok": "OK",
|
||||
"on-object": "auf {object}",
|
||||
"open-console-in-new-tab": "Konsole in neuem Reiter öffnen",
|
||||
"or": "Oder",
|
||||
"page-not-found": "Diese Seite wurde nicht gefunden…",
|
||||
"password": "Passwort",
|
||||
"password-invalid": "Ungültiges Passwort",
|
||||
"pause": "Pausieren",
|
||||
@@ -162,6 +167,7 @@
|
||||
"select-compression": "Kompression auswählen",
|
||||
"select-destination-host": "Zielhost auswählen",
|
||||
"selected-vms-in-execution": "Einige der ausgewählten VMs sind eingeschaltet",
|
||||
"send-ctrl-alt-del": "Ctrl+Alt+Del senden",
|
||||
"send-us-feedback": "Gib uns Feedback",
|
||||
|
||||
"select.network": "Ein Netzwerk auswählen",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"documentation": "Documentation",
|
||||
"edit-config": "Edit config",
|
||||
"enabled": "Enabled",
|
||||
"error-no-data": "Error, can't collect data.",
|
||||
"error-occurred": "An error has occurred",
|
||||
"export": "Export",
|
||||
"export-n-vms": "Export 1 VM | Export {n} VMs",
|
||||
@@ -95,7 +96,9 @@
|
||||
"following-hosts-unreachable": "The following hosts are unreachable",
|
||||
"force-reboot": "Force reboot",
|
||||
"force-shutdown": "Force shutdown",
|
||||
"fullscreen": "Fullscreen",
|
||||
"fullscreen-leave": "Leave fullscreen",
|
||||
"gateway": "Gateway",
|
||||
"n-gb-left": "{n} GB left",
|
||||
"n-gb-required": "{n} GB required",
|
||||
"go-back": "Go back",
|
||||
@@ -106,65 +109,6 @@
|
||||
"host.inactive": "Inactive",
|
||||
|
||||
"invalid-field": "Invalid field",
|
||||
|
||||
"job.vm-copy.bad-power-state": "VM must be halted",
|
||||
"job.vm-copy.in-progress": "Copy in progress…",
|
||||
"job.vm-copy.missing-vms": "No VM to copy",
|
||||
|
||||
"job.vm-delete.bad-power-state": "VM must be halted",
|
||||
"job.vm-delete.in-progress": "Delete in progress…",
|
||||
"job.vm-delete.missing-vms": "No VM to delete",
|
||||
|
||||
"job.vm-force-reboot.bad-power-state": "VM must be running or paused",
|
||||
"job.vm-force-reboot.in-progress": "Force reboot in progress…",
|
||||
"job.vm-force-reboot.missing-vms": "No VM to force reboot",
|
||||
|
||||
"job.vm-force-shutdown.bad-power-state": "VM must be running, suspended or paused",
|
||||
"job.vm-force-shutdown.in-progress": "Force shutdown in progress…",
|
||||
"job.vm-force-shutdown.missing-vms": "No VM to force shutdown",
|
||||
|
||||
"job.vm-export.in-progress": "Export in progress…",
|
||||
"job.vm-export.missing-compression": "Compression must be provided",
|
||||
"job.vm-export.missing-vms": "No VM to export",
|
||||
"job.vm-export.not-allowed": "A VM is not allowed to export",
|
||||
|
||||
"job.vm-migrate.bad-power-state": "VM must be running",
|
||||
"job.vm-migrate.in-progress": "Migrate in progress…",
|
||||
"job.vm-migrate.missing-host": "Destination host must be provided",
|
||||
"job.vm-migrate.missing-vms": "No VM to migrate",
|
||||
|
||||
"job.vm-pause.bad-power-state": "VM must be running",
|
||||
"job.vm-pause.in-progress": "Pause in progress…",
|
||||
"job.vm-pause.missing-vms": "No VM to pause",
|
||||
|
||||
"job.vm-reboot.bad-power-state": "VM must be running",
|
||||
"job.vm-reboot.in-progress": "Reboot in progress…",
|
||||
"job.vm-reboot.missing-vms": "No VM to reboot",
|
||||
|
||||
"job.vm-resume.bad-power-state": "VM must be paused or suspended",
|
||||
"job.vm-resume.in-progress": "Resume in progress…",
|
||||
"job.vm-resume.missing-vms": "No VM to resume",
|
||||
|
||||
"job.vm-shutdown.bad-power-state": "VM must be running",
|
||||
"job.vm-shutdown.in-progress": "Shutdown in progress…",
|
||||
"job.vm-shutdown.missing-vms": "No VM to shutdown",
|
||||
|
||||
"job.vm-snapshot.in-progress": "Snapshot in progress…",
|
||||
"job.vm-snapshot.missing-vms": "No VM to snapshot",
|
||||
|
||||
"job.vm-start.bad-power-state": "VM must be halted",
|
||||
"job.vm-start.in-progress": "Start in progress…",
|
||||
"job.vm-start.missing-vms": "No VM to start",
|
||||
|
||||
"job.vm-start-on.bad-power-state": "VM must be halted",
|
||||
"job.vm-start-on.in-progress": "Start in progress…",
|
||||
"job.vm-start-on.missing-host": "Destination host must be provided",
|
||||
"job.vm-start-on.missing-vms": "No VM to start",
|
||||
|
||||
"job.vm-suspend.bad-power-state": "VM must be running",
|
||||
"job.vm-suspend.in-progress": "Suspend in progress…",
|
||||
"job.vm-suspend.missing-vms": "No VM to suspend",
|
||||
|
||||
"keep-me-logged": "Keep me logged in",
|
||||
"keep-page-open": "Do not refresh or quit tab before end of deployment.",
|
||||
"language": "Language",
|
||||
@@ -195,7 +139,9 @@
|
||||
"object": "Object",
|
||||
"ok": "OK",
|
||||
"on-object": "on {object}",
|
||||
"open-console-in-new-tab": "Open console in new tab",
|
||||
"or": "Or",
|
||||
"page-not-found": "This page is not to be found…",
|
||||
"password": "Password",
|
||||
"password-invalid": "Password invalid",
|
||||
"pause": "Pause",
|
||||
@@ -223,6 +169,7 @@
|
||||
"select-compression": "Select a compression",
|
||||
"select-destination-host": "Select a destination host",
|
||||
"selected-vms-in-execution": "Some selected VMs are running",
|
||||
"send-ctrl-alt-del": "Send Ctrl+Alt+Del",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
|
||||
"select.network": "Select a network",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"documentation": "مستندات",
|
||||
"edit-config": "ویرایش پیکربندی",
|
||||
"enabled": "فعال",
|
||||
"error-no-data": "خطا، نمی توان داده ها را جمع آوری کرد.",
|
||||
"error-occurred": "خطایی رخ داده است",
|
||||
"export": "صادر کردن",
|
||||
"export-n-vms": "صادر کردن 1 ماشین مجازی | {n} ماشین مجازی را صادر کنید",
|
||||
@@ -94,7 +95,9 @@
|
||||
"following-hosts-unreachable": "میزبان های زیر در دسترس نیستند",
|
||||
"force-reboot": "راه اندازی مجدد اجباری",
|
||||
"force-shutdown": "خاموش کردن اجباری",
|
||||
"fullscreen": "تمام صفحه",
|
||||
"fullscreen-leave": "خروج از حالت تمام صفحه",
|
||||
"gateway": "دروازه",
|
||||
"n-gb-left": "{n} GB باقی مانده است",
|
||||
"n-gb-required": "{n} GB مورد نیاز است",
|
||||
"go-back": "برگشت به عقب",
|
||||
@@ -134,7 +137,9 @@
|
||||
"object-not-found": "شیء {id} پیدا نمی شود…",
|
||||
"ok": "اوکی",
|
||||
"on-object": "در {object}",
|
||||
"open-console-in-new-tab": "باز کردن کنسول در برگه جدید",
|
||||
"or": "یا",
|
||||
"page-not-found": "این صفحه پیدا نمی شود…",
|
||||
"password": "کلمه عبور",
|
||||
"password-invalid": "کلمه عبور اشتباه است",
|
||||
"pause": "توقف",
|
||||
@@ -162,6 +167,7 @@
|
||||
"select-compression": "یک فشرده سازی انتخاب کنید",
|
||||
"select-destination-host": "یک میزبان مقصد انتخاب کنید",
|
||||
"selected-vms-in-execution": "برخی از ماشین های مجازی انتخاب شده در حال اجرا هستند",
|
||||
"send-ctrl-alt-del": "ارسال کلیدهای Ctrl+Alt+Del",
|
||||
"send-us-feedback": "برای ما بازخورد ارسال کنید",
|
||||
|
||||
"select.network": "یک شبکه انتخاب کنید",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"documentation": "Documentation",
|
||||
"edit-config": "Modifier config",
|
||||
"enabled": "Activé",
|
||||
"error-no-data": "Erreur, impossible de collecter les données.",
|
||||
"error-occurred": "Une erreur est survenue",
|
||||
"export": "Exporter",
|
||||
"export-n-vms": "Exporter 1 VM | Exporter {n} VMs",
|
||||
@@ -95,7 +96,9 @@
|
||||
"following-hosts-unreachable": "Les hôtes suivants sont inaccessibles",
|
||||
"force-reboot": "Forcer le redémarrage",
|
||||
"force-shutdown": "Forcer l'arrêt",
|
||||
"fullscreen": "Plein écran",
|
||||
"fullscreen-leave": "Quitter plein écran",
|
||||
"gateway": "Passerelle",
|
||||
"n-gb-left": "{n} Go libres",
|
||||
"n-gb-required": "{n} Go requis",
|
||||
"go-back": "Revenir en arrière",
|
||||
@@ -106,65 +109,6 @@
|
||||
"host.inactive": "Inactif | Inactif | Inactifs",
|
||||
|
||||
"invalid-field": "Champ invalide",
|
||||
|
||||
"job.vm-copy.bad-power-state": "La VM doit être arrêtée",
|
||||
"job.vm-copy.in-progress": "Copie en cours…",
|
||||
"job.vm-copy.missing-vms": "Aucune VM à copier",
|
||||
|
||||
"job.vm-delete.bad-power-state": "La VM doit être arrêtée",
|
||||
"job.vm-delete.in-progress": "Suppression en cours…",
|
||||
"job.vm-delete.missing-vms": "Aucune VM à supprimer",
|
||||
|
||||
"job.vm-force-reboot.bad-power-state": "La VM doit être allumée ou en pause",
|
||||
"job.vm-force-reboot.in-progress": "Redémarrage forcé en cours…",
|
||||
"job.vm-force-reboot.missing-vms": "Aucune VM à redémarrer de force",
|
||||
|
||||
"job.vm-force-shutdown.bad-power-state": "La VM doit être allumée, suspendue ou en pause",
|
||||
"job.vm-force-shutdown.in-progress": "Arrêt forcé en cours…",
|
||||
"job.vm-force-shutdown.missing-vms": "Aucune VM à arrêter de force",
|
||||
|
||||
"job.vm-export.in-progress": "Exportation en cours…",
|
||||
"job.vm-export.missing-compression": "La compression est requise",
|
||||
"job.vm-export.missing-vms": "Aucune VM à exporter",
|
||||
"job.vm-export.not-allowed": "VM non autorisée à être exportée",
|
||||
|
||||
"job.vm-migrate.bad-power-state": "La VM doit être allumée",
|
||||
"job.vm-migrate.in-progress": "Migration en cours…",
|
||||
"job.vm-migrate.missing-host": "L'hôte de destination est requis",
|
||||
"job.vm-migrate.missing-vms": "Aucune VM à migrer",
|
||||
|
||||
"job.vm-pause.bad-power-state": "La VM doit être allumée",
|
||||
"job.vm-pause.in-progress": "Mise en pause en cours…",
|
||||
"job.vm-pause.missing-vms": "Aucune VM à mettre en pause",
|
||||
|
||||
"job.vm-reboot.bad-power-state": "La VM doit être allumée",
|
||||
"job.vm-reboot.in-progress": "Redémarrage en cours…",
|
||||
"job.vm-reboot.missing-vms": "Aucune VM à redémarrer",
|
||||
|
||||
"job.vm-resume.bad-power-state": "La VM doit être en pause ou suspendue",
|
||||
"job.vm-resume.in-progress": "Reprise en cours…",
|
||||
"job.vm-resume.missing-vms": "Aucune VM à reprendre",
|
||||
|
||||
"job.vm-shutdown.bad-power-state": "La VM doit être allumée",
|
||||
"job.vm-shutdown.in-progress": "Arrêt en cours…",
|
||||
"job.vm-shutdown.missing-vms": "Aucune VM à arrêter",
|
||||
|
||||
"job.vm-snapshot.in-progress": "Instantané en cours…",
|
||||
"job.vm-snapshot.missing-vms": "Aucune VM pour l'instantané",
|
||||
|
||||
"job.vm-start.bad-power-state": "La VM doit être arrêtée",
|
||||
"job.vm-start.in-progress": "Démarrage en cours…",
|
||||
"job.vm-start.missing-vms": "Aucune VM à démarrer",
|
||||
|
||||
"job.vm-start-on.bad-power-state": "La VM doit être arrêtée",
|
||||
"job.vm-start-on.in-progress": "Démarrage en cours…",
|
||||
"job.vm-start-on.missing-host": "L'hôte de destination est requis",
|
||||
"job.vm-start-on.missing-vms": "Aucune VM à démarrer",
|
||||
|
||||
"job.vm-suspend.bad-power-state": "La VM doit être allumée",
|
||||
"job.vm-suspend.in-progress": "Suspension en cours…",
|
||||
"job.vm-suspend.missing-vms": "Aucune VM à suspendre",
|
||||
|
||||
"keep-me-logged": "Rester connecté",
|
||||
"keep-page-open": "Ne pas rafraichir ou quitter cette page avant la fin du déploiement.",
|
||||
"language": "Langue",
|
||||
@@ -195,7 +139,9 @@
|
||||
"object": "Objet",
|
||||
"ok": "OK",
|
||||
"on-object": "sur {object}",
|
||||
"open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
|
||||
"or": "Ou",
|
||||
"page-not-found": "Cette page est introuvable…",
|
||||
"password": "Mot de passe",
|
||||
"password-invalid": "Mot de passe incorrect",
|
||||
"pause": "Pause",
|
||||
@@ -223,6 +169,7 @@
|
||||
"select-compression": "Sélectionnez une compression",
|
||||
"select-destination-host": "Sélectionnez un hôte de destination",
|
||||
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
|
||||
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
|
||||
"send-us-feedback": "Envoyez-nous vos commentaires",
|
||||
|
||||
"select.network": "Sélectionner un réseau",
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
export default {
|
||||
path: '/host/:uuid',
|
||||
component: () => import('@/views/host/HostRootView.vue'),
|
||||
redirect: { name: 'host.console' },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'host.dashboard',
|
||||
component: () => import('@/views/host/HostDashboardView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'console',
|
||||
name: 'host.console',
|
||||
component: () => import('@/views/host/HostConsoleView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'network',
|
||||
name: 'host.network',
|
||||
component: () => import('@/views/host/HostNetworkView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
name: 'host.tasks',
|
||||
component: () => import('@/views/host/HostTasksView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'vms',
|
||||
name: 'host.vms',
|
||||
component: () => import('@/views/host/HostVmsView.vue'),
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import host from '@/router/host'
|
||||
import pool from '@/router/pool'
|
||||
import story from '@/router/story'
|
||||
import vm from '@/router/vm'
|
||||
@@ -26,7 +25,17 @@ const router = createRouter({
|
||||
story,
|
||||
pool,
|
||||
vm,
|
||||
host,
|
||||
{
|
||||
path: '/host/:uuid',
|
||||
component: () => import('@/views/host/HostRootView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'host.dashboard',
|
||||
component: () => import('@/views/host/HostDashboardView.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
slot('key').help('Meant to receive a title'),
|
||||
slot('value').help('Meant to receive text, tags or other value'),
|
||||
slot('addons').help('Meant to receive icons or other actions buttons '),
|
||||
setting('defaultSlotContent').preset('Uuid').widget(text()),
|
||||
setting('contentSlotContent').preset('71df26a271df26a271df26a271df26a271df26a').widget(text()),
|
||||
]"
|
||||
>
|
||||
<UiCard class="card">
|
||||
<VtsCardRowKeyValue v-bind="properties">
|
||||
<template #key>
|
||||
{{ settings.defaultSlotContent }}
|
||||
</template>
|
||||
<template #value>
|
||||
<span>
|
||||
{{ settings.contentSlotContent }}
|
||||
</span>
|
||||
</template>
|
||||
<template #addons>
|
||||
<VtsIcon accent="warning" :icon="faCircle" :overlay-icon="faStar" />
|
||||
<UiButtonIcon :icon="faCopy" size="medium" accent="info" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
|
||||
<VtsCardRowKeyValue v-bind="properties">
|
||||
<template #key>
|
||||
<span>Network</span>
|
||||
</template>
|
||||
|
||||
<template #value>
|
||||
<UiObjectLink route="`/vm/test/console`">
|
||||
<template #icon>
|
||||
<UiComplexIcon size="medium">
|
||||
<VtsIcon :icon="faNetworkWired" accent="current" />
|
||||
<VtsIcon accent="success" :icon="faCircle" :overlay-icon="faCheck" />
|
||||
</UiComplexIcon>
|
||||
</template>
|
||||
<span>Network Name</span>
|
||||
</UiObjectLink>
|
||||
</template>
|
||||
<template #addons>
|
||||
<UiButtonIcon :icon="faCopy" size="medium" accent="info" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
|
||||
<VtsCardRowKeyValue v-bind="properties">
|
||||
<template #key>
|
||||
<span>Status</span>
|
||||
</template>
|
||||
<template #value>
|
||||
<UiInfo accent="success">Connected</UiInfo>
|
||||
</template>
|
||||
<template #addons>
|
||||
<UiButtonIcon :icon="faCopy" size="medium" accent="info" />
|
||||
<UiButtonIcon :icon="faEllipsis" size="medium" accent="info" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</UiCard>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { setting, slot } from '@/libs/story/story-param'
|
||||
import { text } from '@/libs/story/story-widget'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import UiComplexIcon from '@core/components/ui/complex-icon/UiComplexIcon.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiObjectLink from '@core/components/ui/object-link/UiObjectLink.vue'
|
||||
import { faCheck, faCircle, faCopy, faEllipsis, faNetworkWired, faStar } from '@fortawesome/free-solid-svg-icons'
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.card {
|
||||
width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
prop('status')
|
||||
.enum('connected', 'disconnected', 'partially-connected', 'disconnected-from-physical-device')
|
||||
.required()
|
||||
.preset('connected')
|
||||
.widget(),
|
||||
]"
|
||||
>
|
||||
<VtsConnectionStatus v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { prop } from '@/libs/story/story-param'
|
||||
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[prop('type').required().enum('page', 'card', 'table', 'panel').preset('card').widget()]"
|
||||
:params="[prop('type').required().enum('page', 'card').preset('card').widget()]"
|
||||
>
|
||||
<VtsComingSoonHero v-bind="properties" />
|
||||
</ComponentStory>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[prop('type').required().enum('page', 'card', 'table', 'panel').preset('card').widget()]"
|
||||
>
|
||||
<VtsErrorNoDataHero v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { prop } from '@/libs/story/story-param'
|
||||
import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop('type').required().enum('page', 'card', 'table', 'panel').preset('card').widget(),
|
||||
prop('type').required().enum('page', 'card').preset('card').widget(),
|
||||
prop('disabled').bool().widget(),
|
||||
slot(),
|
||||
setting('slotContent').preset('Content to show when loader is disabled').widget(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[prop('type').required().enum('page', 'card', 'table', 'panel').preset('card').widget()]"
|
||||
:params="[prop('type').required().enum('page', 'card').preset('card').widget()]"
|
||||
>
|
||||
<VtsNoDataHero v-bind="properties" />
|
||||
</ComponentStory>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[prop('type').required().enum('page', 'card', 'panel').preset('card').widget()]"
|
||||
>
|
||||
<VtsNoSelectionHero v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { prop } from '@/libs/story/story-param'
|
||||
import VtsNoSelectionHero from '@core/components/state-hero/VtsNoSelectionHero.vue'
|
||||
</script>
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
prop('id').required().str().preset('123-456-789').widget(),
|
||||
prop('type').required().enum('page', 'card', 'table', 'panel').preset('card').widget(),
|
||||
]"
|
||||
>
|
||||
<ComponentStory v-slot="{ properties }" :params="[prop('id').required().str().preset('123-456-789').widget()]">
|
||||
<VtsObjectNotFoundHero v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop('type').required().enum('page', 'card', 'panel', 'table').preset('card').widget(),
|
||||
prop('type').required().enum('page', 'card').preset('card').widget(),
|
||||
prop('busy').bool().widget(),
|
||||
prop('image').enum('no-result', 'under-construction', 'no-selection', 'error', 'no-data').widget(),
|
||||
prop('image').enum('no-result', 'under-construction').widget(),
|
||||
slot(),
|
||||
setting('defaultSlot').preset('Some text').widget(),
|
||||
]"
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop('icon')
|
||||
.type('IconDefinition')
|
||||
.widget(
|
||||
choice(
|
||||
{ label: 'Ship', value: faShip },
|
||||
{ label: 'Rocket', value: faRocket },
|
||||
{ label: 'Floppy', value: faFloppyDisk },
|
||||
{ label: 'Filter', value: faFilter }
|
||||
)
|
||||
),
|
||||
prop('selected').bool().widget(),
|
||||
prop('disabled').bool().widget().ctx(),
|
||||
slot(),
|
||||
setting('defaultSlotContent').preset('Dropdown title').widget(text()).help('Content for default slot'),
|
||||
]"
|
||||
>
|
||||
<UiDropdownButton v-bind="properties">{{ settings.defaultSlotContent }}</UiDropdownButton>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { prop, setting, slot } from '@/libs/story/story-param'
|
||||
import { choice, text } from '@/libs/story/story-widget'
|
||||
import UiDropdownButton from '@core/components/ui/dropdown-button/UiDropdownButton.vue'
|
||||
import { faFloppyDisk, faRocket, faShip, faFilter } from '@fortawesome/free-solid-svg-icons'
|
||||
</script>
|
||||
@@ -3,14 +3,11 @@
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop('accent').enum('info', 'success', 'warning', 'danger').required().preset('info').widget(),
|
||||
prop('wrap').bool().help('Choose if the text should wrap if too long').widget(),
|
||||
slot(),
|
||||
setting('defaultSlot').widget(text()).preset('message'),
|
||||
]"
|
||||
>
|
||||
<div class="wrapper">
|
||||
<UiInfo v-bind="properties">{{ settings.defaultSlot }}</UiInfo>
|
||||
</div>
|
||||
<UiInfo v-bind="properties">{{ settings.defaultSlot }}</UiInfo>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
@@ -20,9 +17,3 @@ import { prop, setting, slot } from '@/libs/story/story-param'
|
||||
import { text } from '@/libs/story/story-widget'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.wrapper {
|
||||
max-width: 30rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,19 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().type('string | number').preset(''),
|
||||
prop('accent').enum('info', 'warning', 'danger').required().preset('info').widget(),
|
||||
prop('id').str().widget(),
|
||||
prop('type').enum('text', 'number', 'password').preset('text').widget(),
|
||||
model().type('string').preset(''),
|
||||
prop('disabled').bool().widget(),
|
||||
prop('clearable').bool().widget(),
|
||||
prop('href').str().widget(),
|
||||
prop('label').str().widget(),
|
||||
prop('info').str().widget(),
|
||||
iconProp(),
|
||||
iconProp('labelIcon'),
|
||||
prop('placeholder').str().widget(),
|
||||
prop('required').bool().widget(),
|
||||
slot(),
|
||||
slot('info'),
|
||||
setting('defaultSlot').widget(text()).preset('Some label'),
|
||||
setting('info').widget(text()).preset('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'),
|
||||
iconProp().preset(faMagnifyingGlass).type('IconDefinition'),
|
||||
]"
|
||||
>
|
||||
<UiInput placeholder="Enter your search" v-bind="properties">
|
||||
{{ settings.defaultSlot }}
|
||||
<template v-if="settings.info" #info>{{ settings.info }}</template>
|
||||
</UiInput>
|
||||
<UiInput placeholder="Enter your search" v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { iconProp, model, prop, setting, slot } from '@/libs/story/story-param'
|
||||
import { text } from '@/libs/story/story-widget'
|
||||
import { iconProp, model, prop } from '@/libs/story/story-param'
|
||||
import UiInput from '@core/components/ui/input/UiInput.vue'
|
||||
import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop('accent').enum('neutral', 'warning', 'danger').required().preset('neutral').widget(),
|
||||
prop('for').str().widget(),
|
||||
prop('required').bool().widget(),
|
||||
prop('href').str().widget(),
|
||||
slot(),
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
prop('size').required().enum('small', 'medium').preset('medium').widget(),
|
||||
prop('to').str().widget(),
|
||||
prop('href').str().widget(),
|
||||
iconProp(),
|
||||
prop('disabled').bool().widget(),
|
||||
prop('target').enum('_blank', '_self').widget(),
|
||||
]"
|
||||
>
|
||||
<UiLink v-bind="properties">This is a link</UiLink>
|
||||
|
||||
<div v-if="!properties.to && !properties.href" class="info">
|
||||
<VtsIcon :icon="faInfoCircle" accent="brand" />
|
||||
Link is disabled because no `href` or `to` is provided
|
||||
</div>
|
||||
<div v-else-if="properties.to && properties.href" class="info">
|
||||
<VtsIcon :icon="faExclamationTriangle" accent="brand" />
|
||||
`to` is ignored when `href` is provided
|
||||
</div>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { iconProp, prop } from '@/libs/story/story-param'
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { faExclamationTriangle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.info {
|
||||
margin-top: 4rem;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +0,0 @@
|
||||
```typescript
|
||||
type PaginationPayload = {
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
}
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
prop('total-items').required().preset(100).num().widget(),
|
||||
prop('disabled').bool().widget(),
|
||||
event('change').args({ payload: 'PaginationPayload' }),
|
||||
]"
|
||||
>
|
||||
<UiTablePagination v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { event, prop } from '@/libs/story/story-param'
|
||||
import UiTablePagination from '@core/components/ui/table-pagination/UiTablePagination.vue'
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ settings }"
|
||||
:params="[slot().help('Meant to receive a list of UiTag components'), setting('slot').preset('Label').widget()]"
|
||||
>
|
||||
<UiTagsList>
|
||||
<UiTag accent="info" variant="primary">{{ settings.slot }}</UiTag>
|
||||
<UiTag accent="success" variant="primary">{{ settings.slot }}</UiTag>
|
||||
<UiTag accent="warning" variant="secondary">{{ settings.slot }}</UiTag>
|
||||
<UiTag accent="danger" variant="secondary">{{ settings.slot }}</UiTag>
|
||||
</UiTagsList>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { setting, slot } from '@/libs/story/story-param'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
|
||||
</script>
|
||||
@@ -1,178 +0,0 @@
|
||||
<template>
|
||||
<div :class="{ 'no-ui': !uiStore.hasUi }" class="host-console-view">
|
||||
<div v-if="hasError">{{ $t('error-occurred') }}</div>
|
||||
<UiSpinner v-else-if="!isReady" class="spinner" />
|
||||
<UiStatusPanel v-else-if="!isHostRunning" :image-source="monitor" :title="$t('power-on-host-for-console')" />
|
||||
<template v-else-if="host && hostConsole">
|
||||
<VtsLayoutConsole>
|
||||
<RemoteConsole
|
||||
ref="consoleElement"
|
||||
:is-console-available="isConsoleAvailable"
|
||||
:location="hostConsole.location"
|
||||
class="remote-console"
|
||||
/>
|
||||
<template #actions>
|
||||
<VtsActionsConsole
|
||||
:open-in-new-tab="openInNewTab"
|
||||
:send-ctrl-alt-del="sendCtrlAltDel"
|
||||
:toggle-full-screen="toggleFullScreen"
|
||||
:is-fullscreen="!uiStore.hasUi"
|
||||
/>
|
||||
<VtsDivider type="stretch" />
|
||||
<VtsClipboardConsole />
|
||||
</template>
|
||||
</VtsLayoutConsole>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import monitor from '@/assets/monitor.svg'
|
||||
import RemoteConsole from '@/components/RemoteConsole.vue'
|
||||
import UiSpinner from '@/components/ui/UiSpinner.vue'
|
||||
import UiStatusPanel from '@/components/ui/UiStatusPanel.vue'
|
||||
import { isHostOperationPending } from '@/libs/host'
|
||||
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums'
|
||||
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
|
||||
import { usePageTitleStore } from '@/stores/page-title.store'
|
||||
import { useConsoleStore } from '@/stores/xen-api/console.store'
|
||||
import { useControlDomainStore } from '@/stores/xen-api/control-domain.store'
|
||||
import { useHostStore } from '@/stores/xen-api/host.store'
|
||||
import VtsActionsConsole from '@core/components/console/VtsActionsConsole.vue'
|
||||
import VtsClipboardConsole from '@core/components/console/VtsClipboardConsole.vue'
|
||||
import VtsLayoutConsole from '@core/components/console/VtsLayoutConsole.vue'
|
||||
import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import { useUiStore } from '@core/stores/ui.store'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const STOP_OPERATIONS = [HOST_OPERATION.SHUTDOWN]
|
||||
|
||||
usePageTitleStore().setTitle(useI18n().t('console'))
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const {
|
||||
isReady: isHostReady,
|
||||
getByUuid: getHostByUuid,
|
||||
hasError: hasHostError,
|
||||
runningHosts,
|
||||
} = useHostStore().subscribe()
|
||||
const { records: controlDomains } = useControlDomainStore().subscribe()
|
||||
|
||||
const {
|
||||
isReady: isConsoleReady,
|
||||
getByOpaqueRef: getConsoleByOpaqueRef,
|
||||
hasError: hasConsoleError,
|
||||
} = useConsoleStore().subscribe()
|
||||
|
||||
const hasError = computed(() => hasHostError.value || hasConsoleError.value)
|
||||
|
||||
const host = computed(() => getHostByUuid(route.params.uuid as XenApiHost['uuid']))
|
||||
|
||||
const controlDomain = computed(() => {
|
||||
const controlDomainOpaqueRef = host.value?.control_domain
|
||||
return controlDomainOpaqueRef
|
||||
? controlDomains.value.find(controlDomain => controlDomain.$ref === controlDomainOpaqueRef)
|
||||
: undefined
|
||||
})
|
||||
|
||||
const hostConsole = computed(() => {
|
||||
const consoleOpaqueRef = controlDomain.value?.consoles[0]
|
||||
return consoleOpaqueRef ? getConsoleByOpaqueRef(consoleOpaqueRef) : undefined
|
||||
})
|
||||
|
||||
const isReady = computed(() => isHostReady.value && isConsoleReady.value && controlDomain.value)
|
||||
|
||||
const isHostRunning = computed(() => {
|
||||
return runningHosts.value.some(runningHost => runningHost.uuid === host.value?.uuid)
|
||||
})
|
||||
|
||||
const isConsoleAvailable = computed(() =>
|
||||
controlDomain.value !== undefined ? !isHostOperationPending(host.value!, STOP_OPERATIONS) : false
|
||||
)
|
||||
|
||||
const consoleElement = ref()
|
||||
|
||||
const sendCtrlAltDel = () => consoleElement.value?.sendCtrlAltDel()
|
||||
|
||||
const toggleFullScreen = () => {
|
||||
uiStore.hasUi = !uiStore.hasUi
|
||||
}
|
||||
|
||||
const openInNewTab = () => {
|
||||
const routeData = router.resolve({ query: { ui: '0' } })
|
||||
window.open(routeData.href, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.host-console-view {
|
||||
display: flex;
|
||||
height: calc(100% - 14.5rem);
|
||||
flex-direction: column;
|
||||
|
||||
&.no-ui {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
color: var(--color-info-txt-base);
|
||||
display: flex;
|
||||
margin: auto;
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.remote-console {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.not-available {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 4rem;
|
||||
color: var(--color-info-txt-base);
|
||||
font-size: 3.6rem;
|
||||
}
|
||||
|
||||
.open-in-new-window {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
|
||||
& > .link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
background-color: var(--color-info-txt-base);
|
||||
color: var(--color-info-txt-item);
|
||||
text-decoration: none;
|
||||
padding: 1.5rem;
|
||||
font-size: 1.6rem;
|
||||
border-radius: 0 0 0 0.8rem;
|
||||
white-space: nowrap;
|
||||
transform: translateX(calc(100% - 4.5rem));
|
||||
transition: transform 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-console-view:deep(.menu-list) {
|
||||
background-color: transparent;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from '@/components/PageUnderConstruction.vue'
|
||||
import { usePageTitleStore } from '@/stores/page-title.store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
usePageTitleStore().setTitle(useI18n().t('network'))
|
||||
</script>
|
||||
@@ -1,16 +1,10 @@
|
||||
<template>
|
||||
<ObjectNotFoundWrapper :is-ready="isReady" :uuid-checker="hasUuid">
|
||||
<template v-if="uiStore.hasUi">
|
||||
<HostHeader :host="host!" />
|
||||
<HostTabBar :uuid="host!.uuid" />
|
||||
</template>
|
||||
<RouterView />
|
||||
</ObjectNotFoundWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HostHeader from '@/components/host/HostHeader.vue'
|
||||
import HostTabBar from '@/components/host/HostTabBar.vue'
|
||||
import ObjectNotFoundWrapper from '@/components/ObjectNotFoundWrapper.vue'
|
||||
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
|
||||
import { usePageTitleStore } from '@/stores/page-title.store'
|
||||
@@ -23,10 +17,11 @@ const { hasUuid, isReady, getByUuid } = useHostStore().subscribe()
|
||||
const route = useRoute()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const host = computed(() => getByUuid(route.params.uuid as XenApiHost['uuid']))
|
||||
const currentHost = computed(() => getByUuid(route.params.uuid as XenApiHost['uuid']))
|
||||
|
||||
watchEffect(() => {
|
||||
uiStore.currentHostOpaqueRef = host.value?.$ref
|
||||
uiStore.currentHostOpaqueRef = currentHost.value?.$ref
|
||||
})
|
||||
|
||||
usePageTitleStore().setObject(host)
|
||||
usePageTitleStore().setObject(currentHost)
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user