2119 lines
215 KiB
HTML
2119 lines
215 KiB
HTML
|
<!DOCTYPE html><html lang="fr">
|
|||
|
<head><meta charset="utf-8">
|
|||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"><title>r-evolutions-dans-le-monde-de-la-sauvegarde-de-donnees - YannStatic</title>
|
|||
|
|
|||
|
<meta name="description" content="URL: http://linuxfr.org/news/r-evolutions-dans-le-monde-de-la-sauvegarde-de-donneesTitle: (R)évolutions dans le monde de la sauvegarde de donnéesAuthor...">
|
|||
|
<link rel="canonical" href="https://static.rnmkcy.eu/2019/12/25/r-evolutions-dans-le-monde-de-la-sauvegarde-de-donnees.html"><link rel="alternate" type="application/rss+xml" title="YannStatic" href="/feed.xml">
|
|||
|
|
|||
|
<!-- - include head/favicon.html - -->
|
|||
|
<link rel="shortcut icon" type="image/png" href="/assets/favicon/favicon.png"><link rel="stylesheet" href="/assets/css/main.css"><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" ><!-- start custom head snippets --><link rel="stylesheet" href="/assets/css/expand.css">
|
|||
|
<!-- end custom head snippets --><script>(function() {
|
|||
|
window.isArray = function(val) {
|
|||
|
return Object.prototype.toString.call(val) === '[object Array]';
|
|||
|
};
|
|||
|
window.isString = function(val) {
|
|||
|
return typeof val === 'string';
|
|||
|
};
|
|||
|
|
|||
|
window.hasEvent = function(event) {
|
|||
|
return 'on'.concat(event) in window.document;
|
|||
|
};
|
|||
|
|
|||
|
window.isOverallScroller = function(node) {
|
|||
|
return node === document.documentElement || node === document.body || node === window;
|
|||
|
};
|
|||
|
|
|||
|
window.isFormElement = function(node) {
|
|||
|
var tagName = node.tagName;
|
|||
|
return tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
|
|||
|
};
|
|||
|
|
|||
|
window.pageLoad = (function () {
|
|||
|
var loaded = false, cbs = [];
|
|||
|
window.addEventListener('load', function () {
|
|||
|
var i;
|
|||
|
loaded = true;
|
|||
|
if (cbs.length > 0) {
|
|||
|
for (i = 0; i < cbs.length; i++) {
|
|||
|
cbs[i]();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
return {
|
|||
|
then: function(cb) {
|
|||
|
cb && (loaded ? cb() : (cbs.push(cb)));
|
|||
|
}
|
|||
|
};
|
|||
|
})();
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
window.throttle = function(func, wait) {
|
|||
|
var args, result, thisArg, timeoutId, lastCalled = 0;
|
|||
|
|
|||
|
function trailingCall() {
|
|||
|
lastCalled = new Date;
|
|||
|
timeoutId = null;
|
|||
|
result = func.apply(thisArg, args);
|
|||
|
}
|
|||
|
return function() {
|
|||
|
var now = new Date,
|
|||
|
remaining = wait - (now - lastCalled);
|
|||
|
|
|||
|
args = arguments;
|
|||
|
thisArg = this;
|
|||
|
|
|||
|
if (remaining <= 0) {
|
|||
|
clearTimeout(timeoutId);
|
|||
|
timeoutId = null;
|
|||
|
lastCalled = now;
|
|||
|
result = func.apply(thisArg, args);
|
|||
|
} else if (!timeoutId) {
|
|||
|
timeoutId = setTimeout(trailingCall, remaining);
|
|||
|
}
|
|||
|
return result;
|
|||
|
};
|
|||
|
};
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var Set = (function() {
|
|||
|
var add = function(item) {
|
|||
|
var i, data = this._data;
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
if (data[i] === item) {
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
this.size ++;
|
|||
|
data.push(item);
|
|||
|
return data;
|
|||
|
};
|
|||
|
|
|||
|
var Set = function(data) {
|
|||
|
this.size = 0;
|
|||
|
this._data = [];
|
|||
|
var i;
|
|||
|
if (data.length > 0) {
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
add.call(this, data[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
Set.prototype.add = add;
|
|||
|
Set.prototype.get = function(index) { return this._data[index]; };
|
|||
|
Set.prototype.has = function(item) {
|
|||
|
var i, data = this._data;
|
|||
|
for (i = 0; i < data.length; i++) {
|
|||
|
if (this.get(i) === item) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
};
|
|||
|
Set.prototype.is = function(map) {
|
|||
|
if (map._data.length !== this._data.length) { return false; }
|
|||
|
var i, j, flag, tData = this._data, mData = map._data;
|
|||
|
for (i = 0; i < tData.length; i++) {
|
|||
|
for (flag = false, j = 0; j < mData.length; j++) {
|
|||
|
if (tData[i] === mData[j]) {
|
|||
|
flag = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (!flag) { return false; }
|
|||
|
}
|
|||
|
return true;
|
|||
|
};
|
|||
|
Set.prototype.values = function() {
|
|||
|
return this._data;
|
|||
|
};
|
|||
|
return Set;
|
|||
|
})();
|
|||
|
|
|||
|
window.Lazyload = (function(doc) {
|
|||
|
var queue = {js: [], css: []}, sources = {js: {}, css: {}}, context = this;
|
|||
|
var createNode = function(name, attrs) {
|
|||
|
var node = doc.createElement(name), attr;
|
|||
|
for (attr in attrs) {
|
|||
|
if (attrs.hasOwnProperty(attr)) {
|
|||
|
node.setAttribute(attr, attrs[attr]);
|
|||
|
}
|
|||
|
}
|
|||
|
return node;
|
|||
|
};
|
|||
|
var end = function(type, url) {
|
|||
|
var s, q, qi, cbs, i, j, cur, val, flag;
|
|||
|
if (type === 'js' || type ==='css') {
|
|||
|
s = sources[type], q = queue[type];
|
|||
|
s[url] = true;
|
|||
|
for (i = 0; i < q.length; i++) {
|
|||
|
cur = q[i];
|
|||
|
if (cur.urls.has(url)) {
|
|||
|
qi = cur, val = qi.urls.values();
|
|||
|
qi && (cbs = qi.callbacks);
|
|||
|
for (flag = true, j = 0; j < val.length; j++) {
|
|||
|
cur = val[j];
|
|||
|
if (!s[cur]) {
|
|||
|
flag = false;
|
|||
|
}
|
|||
|
}
|
|||
|
if (flag && cbs && cbs.length > 0) {
|
|||
|
for (j = 0; j < cbs.length; j++) {
|
|||
|
cbs[j].call(context);
|
|||
|
}
|
|||
|
qi.load = true;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
var load = function(type, urls, callback) {
|
|||
|
var s, q, qi, node, i, cur,
|
|||
|
_urls = typeof urls === 'string' ? new Set([urls]) : new Set(urls), val, url;
|
|||
|
if (type === 'js' || type ==='css') {
|
|||
|
s = sources[type], q = queue[type];
|
|||
|
for (i = 0; i < q.length; i++) {
|
|||
|
cur = q[i];
|
|||
|
if (_urls.is(cur.urls)) {
|
|||
|
qi = cur;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
val = _urls.values();
|
|||
|
if (qi) {
|
|||
|
callback && (qi.load || qi.callbacks.push(callback));
|
|||
|
callback && (qi.load && callback());
|
|||
|
} else {
|
|||
|
q.push({
|
|||
|
urls: _urls,
|
|||
|
callbacks: callback ? [callback] : [],
|
|||
|
load: false
|
|||
|
});
|
|||
|
for (i = 0; i < val.length; i++) {
|
|||
|
node = null, url = val[i];
|
|||
|
if (s[url] === undefined) {
|
|||
|
(type === 'js' ) && (node = createNode('script', { src: url }));
|
|||
|
(type === 'css') && (node = createNode('link', { rel: 'stylesheet', href: url }));
|
|||
|
if (node) {
|
|||
|
node.onload = (function(type, url) {
|
|||
|
return function() {
|
|||
|
end(type, url);
|
|||
|
};
|
|||
|
})(type, url);
|
|||
|
(doc.head || doc.body).appendChild(node);
|
|||
|
s[url] = false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
return {
|
|||
|
js: function(url, callback) {
|
|||
|
load('js', url, callback);
|
|||
|
},
|
|||
|
css: function(url, callback) {
|
|||
|
load('css', url, callback);
|
|||
|
}
|
|||
|
};
|
|||
|
})(this.document);
|
|||
|
})();
|
|||
|
</script><script>
|
|||
|
(function() {
|
|||
|
var TEXT_VARIABLES = {
|
|||
|
version: '2.2.6',
|
|||
|
sources: {
|
|||
|
font_awesome: 'https://use.fontawesome.com/releases/v5.0.13/css/all.css',
|
|||
|
jquery: '/assets/js/jquery.min.js',
|
|||
|
leancloud_js_sdk: '//cdn.jsdelivr.net/npm/leancloud-storage@3.13.2/dist/av-min.js',
|
|||
|
chart: 'https://cdn.bootcss.com/Chart.js/2.7.2/Chart.bundle.min.js',
|
|||
|
gitalk: {
|
|||
|
js: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.js',
|
|||
|
css: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.css'
|
|||
|
},
|
|||
|
valine: 'https://unpkg.com/valine/dist/Valine.min.js'
|
|||
|
},
|
|||
|
site: {
|
|||
|
toc: {
|
|||
|
selectors: 'h1,h2,h3'
|
|||
|
}
|
|||
|
},
|
|||
|
paths: {
|
|||
|
search_js: '/assets/search.js'
|
|||
|
}
|
|||
|
};
|
|||
|
window.TEXT_VARIABLES = TEXT_VARIABLES;
|
|||
|
})();
|
|||
|
</script>
|
|||
|
</head>
|
|||
|
<body>
|
|||
|
<div class="root" data-is-touch="false">
|
|||
|
<div class="layout--page js-page-root"><!----><div class="page__main js-page-main page__viewport hide-footer has-aside has-aside cell cell--auto">
|
|||
|
|
|||
|
<div class="page__main-inner"><div class="page__header d-print-none"><header class="header"><div class="main">
|
|||
|
<div class="header__title">
|
|||
|
<div class="header__brand"><svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="478.9473684210526" viewBox="0, 0, 400,478.9473684210526"><g id="svgg"><path id="path0" d="M308.400 56.805 C 306.970 56.966,303.280 57.385,300.200 57.738 C 290.906 58.803,278.299 59.676,269.200 59.887 L 260.600 60.085 259.400 61.171 C 258.010 62.428,256.198 63.600,255.645 63.600 C 255.070 63.600,252.887 65.897,252.598 66.806 C 252.460 67.243,252.206 67.600,252.034 67.600 C 251.397 67.600,247.206 71.509,247.202 72.107 C 247.201 72.275,246.390 73.190,245.400 74.138 C 243.961 75.517,243.598 76.137,243.592 77.231 C 243.579 79.293,241.785 83.966,240.470 85.364 C 239.176 86.740,238.522 88.365,237.991 91.521 C 237.631 93.665,236.114 97.200,235.554 97.200 C 234.938 97.200,232.737 102.354,232.450 104.472 C 232.158 106.625,230.879 109.226,229.535 110.400 C 228.933 110.926,228.171 113.162,226.434 119.500 C 226.178 120.435,225.795 121.200,225.584 121.200 C 225.373 121.200,225.200 121.476,225.200 121.813 C 225.200 122.149,224.885 122.541,224.500 122.683 C 223.606 123.013,223.214 123.593,223.204 124.600 C 223.183 126.555,220.763 132.911,219.410 134.562 C 218.443 135.742,217.876 136.956,217.599 138.440 C 217.041 141.424,215.177 146.434,214.532 146.681 C 214.240 146.794,214.000 147.055,214.000 147.261 C 214.000 147.467,213.550 148.086,213.000 148.636 C 212.450 149.186,212.000 149.893,212.000 150.208 C 212.000 151.386,208.441 154.450,207.597 153.998 C 206.319 153.315,204.913 150.379,204.633 147.811 C 204.365 145.357,202.848 142.147,201.759 141.729 C 200.967 141.425,199.200 137.451,199.200 135.974 C 199.200 134.629,198.435 133.224,196.660 131.311 C 195.363 129.913,194.572 128.123,193.870 125.000 C 193.623 123.900,193.236 122.793,193.010 122.540 C 190.863 120.133,190.147 118.880,188.978 115.481 C 188.100 112.928,187.151 111.003,186.254 109.955 C 185.358 108.908,184.518 107.204,183.847 105.073 C 183.280 103.273,182.497 101.329,182.108 100.753 C 181.719 100.177,180.904 98.997,180.298 98.131 C 179.693 97.265,178.939 95.576,178.624 94.378 C 178.041 92.159,177.125 90.326,175.023 87.168 C 174.375 86.196,173.619 84.539,173.342 83.486 C 172.800 81.429,171.529 79.567,170.131 78.785 C 169.654 78.517,168.697 77.511,168.006 76.549 C 167.316 75.587,166.594 74.800,166.402 74.800 C 166.210 74.800,164.869 73.633,163.421 72.206 C 160.103 68.936,161.107 69.109,146.550 69.301 C 133.437 69.474,128.581 70.162,126.618 72.124 C 126.248 72.495,125.462 72.904,124.872 73.033 C 124.282 73.163,123.088 73.536,122.219 73.863 C 121.349 74.191,119.028 74.638,117.061 74.858 C 113.514 75.254,109.970 76.350,108.782 77.419 C 107.652 78.436,100.146 80.400,97.388 80.400 C 95.775 80.400,93.167 81.360,91.200 82.679 C 90.430 83.195,89.113 83.804,88.274 84.031 C 85.875 84.681,78.799 90.910,74.400 96.243 L 73.400 97.456 73.455 106.028 C 73.526 117.055,74.527 121.238,77.820 124.263 C 78.919 125.273,80.400 127.902,80.400 128.842 C 80.400 129.202,81.075 130.256,81.900 131.186 C 83.563 133.059,85.497 136.346,86.039 138.216 C 86.233 138.886,87.203 140.207,88.196 141.153 C 89.188 142.098,90.000 143.104,90.000 143.388 C 90.000 144.337,92.129 148.594,92.869 149.123 C 93.271 149.410,93.600 149.831,93.600 150.059 C 93.600 150.286,93.932 150.771,94.337 151.136 C 94.743 151.501,95.598 153.004,96.237 154.475 C 96.877 155.947,97.760 157.351,98.200 157.596 C 98.640 157.841,99.900 159.943,101.000 162.267 C 102.207 164.817,103.327 166.644,103.825 166.876 C 104.278 167.087,105.065 168.101,105.573 169.130 C 107.658 173.348,108.097 174.093,110.006 176.647 C 111.103 178.114,112.000 179.725,112.000 180.227 C 112.000 181.048,113.425 183.163,114.678 184.200 C 115.295 184.711,117.396 188.733,117.720 190.022 C 117.855 190.562,118.603 191.633,119.381 192.402 C 120.160 193.171,121.496 195.258,122.351 197.039 C 123.206 198.820,124.167 200.378,124.487 200.501 C 124.807 200.624,125.953 202.496,127.034 204.662 C 128.114 206.828,129.676 209.299,130.505 210.153 C 131.333 211.007,132.124 212.177,132.262 212.753 C 132.618 214.239,134.291 217.048,136.288 219.5
|
|||
|
" href="/">YannStatic</a></div><!--<button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button>--><!-- <li><button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button></li> -->
|
|||
|
<!-- Champ de recherche -->
|
|||
|
<div id="searchbox" class="search search--dark" style="visibility: visible">
|
|||
|
<div class="main">
|
|||
|
<div class="search__header"></div>
|
|||
|
<div class="search-bar">
|
|||
|
<div class="search-box js-search-box">
|
|||
|
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
|
|||
|
<input id="search-input" type="text" />
|
|||
|
<!-- <div class="search-box__icon-clear js-icon-clear">
|
|||
|
<a><i class="fas fa-times"></i></a>
|
|||
|
</div> -->
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<!-- Script pointing to search-script.js -->
|
|||
|
<script>/*!
|
|||
|
* Simple-Jekyll-Search
|
|||
|
* Copyright 2015-2020, Christian Fei
|
|||
|
* Licensed under the MIT License.
|
|||
|
*/
|
|||
|
|
|||
|
(function(){
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Templater_7 = {
|
|||
|
compile: compile,
|
|||
|
setOptions: setOptions
|
|||
|
}
|
|||
|
|
|||
|
const options = {}
|
|||
|
options.pattern = /\{(.*?)\}/g
|
|||
|
options.template = ''
|
|||
|
options.middleware = function () {}
|
|||
|
|
|||
|
function setOptions (_options) {
|
|||
|
options.pattern = _options.pattern || options.pattern
|
|||
|
options.template = _options.template || options.template
|
|||
|
if (typeof _options.middleware === 'function') {
|
|||
|
options.middleware = _options.middleware
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function compile (data) {
|
|||
|
return options.template.replace(options.pattern, function (match, prop) {
|
|||
|
const value = options.middleware(prop, data[prop], options.template)
|
|||
|
if (typeof value !== 'undefined') {
|
|||
|
return value
|
|||
|
}
|
|||
|
return data[prop] || match
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
'use strict';
|
|||
|
|
|||
|
function fuzzysearch (needle, haystack) {
|
|||
|
var tlen = haystack.length;
|
|||
|
var qlen = needle.length;
|
|||
|
if (qlen > tlen) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (qlen === tlen) {
|
|||
|
return needle === haystack;
|
|||
|
}
|
|||
|
outer: for (var i = 0, j = 0; i < qlen; i++) {
|
|||
|
var nch = needle.charCodeAt(i);
|
|||
|
while (j < tlen) {
|
|||
|
if (haystack.charCodeAt(j++) === nch) {
|
|||
|
continue outer;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
var _$fuzzysearch_1 = fuzzysearch;
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
|
|||
|
|
|||
|
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
|
|||
|
|
|||
|
function FuzzySearchStrategy () {
|
|||
|
this.matches = function (string, crit) {
|
|||
|
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
|
|||
|
|
|||
|
function LiteralSearchStrategy () {
|
|||
|
this.matches = function (str, crit) {
|
|||
|
if (!str) return false
|
|||
|
|
|||
|
str = str.trim().toLowerCase()
|
|||
|
crit = crit.trim().toLowerCase()
|
|||
|
|
|||
|
return crit.split(' ').filter(function (word) {
|
|||
|
return str.indexOf(word) >= 0
|
|||
|
}).length === crit.split(' ').length
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Repository_4 = {
|
|||
|
put: put,
|
|||
|
clear: clear,
|
|||
|
search: search,
|
|||
|
setOptions: __setOptions_4
|
|||
|
}
|
|||
|
|
|||
|
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
|
|||
|
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
|
|||
|
|
|||
|
function NoSort () {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
const data = []
|
|||
|
let opt = {}
|
|||
|
|
|||
|
opt.fuzzy = false
|
|||
|
opt.limit = 10
|
|||
|
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = NoSort
|
|||
|
opt.exclude = []
|
|||
|
|
|||
|
function put (data) {
|
|||
|
if (isObject(data)) {
|
|||
|
return addObject(data)
|
|||
|
}
|
|||
|
if (isArray(data)) {
|
|||
|
return addArray(data)
|
|||
|
}
|
|||
|
return undefined
|
|||
|
}
|
|||
|
function clear () {
|
|||
|
data.length = 0
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function isObject (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
|
|||
|
}
|
|||
|
|
|||
|
function isArray (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
|
|||
|
}
|
|||
|
|
|||
|
function addObject (_data) {
|
|||
|
data.push(_data)
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function addArray (_data) {
|
|||
|
const added = []
|
|||
|
clear()
|
|||
|
for (let i = 0, len = _data.length; i < len; i++) {
|
|||
|
if (isObject(_data[i])) {
|
|||
|
added.push(addObject(_data[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
return added
|
|||
|
}
|
|||
|
|
|||
|
function search (crit) {
|
|||
|
if (!crit) {
|
|||
|
return []
|
|||
|
}
|
|||
|
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
|
|||
|
}
|
|||
|
|
|||
|
function __setOptions_4 (_opt) {
|
|||
|
opt = _opt || {}
|
|||
|
|
|||
|
opt.fuzzy = _opt.fuzzy || false
|
|||
|
opt.limit = _opt.limit || 10
|
|||
|
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = _opt.sort || NoSort
|
|||
|
opt.exclude = _opt.exclude || []
|
|||
|
}
|
|||
|
|
|||
|
function findMatches (data, crit, strategy, opt) {
|
|||
|
const matches = []
|
|||
|
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
|
|||
|
const match = findMatchesInObject(data[i], crit, strategy, opt)
|
|||
|
if (match) {
|
|||
|
matches.push(match)
|
|||
|
}
|
|||
|
}
|
|||
|
return matches
|
|||
|
}
|
|||
|
|
|||
|
function findMatchesInObject (obj, crit, strategy, opt) {
|
|||
|
for (const key in obj) {
|
|||
|
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
|
|||
|
return obj
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isExcluded (term, excludedTerms) {
|
|||
|
for (let i = 0, len = excludedTerms.length; i < len; i++) {
|
|||
|
const excludedTerm = excludedTerms[i]
|
|||
|
if (new RegExp(excludedTerm).test(term)) {
|
|||
|
return true
|
|||
|
}
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
/* globals ActiveXObject:false */
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$JSONLoader_2 = {
|
|||
|
load: load
|
|||
|
}
|
|||
|
|
|||
|
function load (location, callback) {
|
|||
|
const xhr = getXHR()
|
|||
|
xhr.open('GET', location, true)
|
|||
|
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
|
|||
|
xhr.send()
|
|||
|
}
|
|||
|
|
|||
|
function createStateChangeListener (xhr, callback) {
|
|||
|
return function () {
|
|||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|||
|
try {
|
|||
|
callback(null, JSON.parse(xhr.responseText))
|
|||
|
} catch (err) {
|
|||
|
callback(err, null)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function getXHR () {
|
|||
|
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$OptionsValidator_3 = function OptionsValidator (params) {
|
|||
|
if (!validateParams(params)) {
|
|||
|
throw new Error('-- OptionsValidator: required options missing')
|
|||
|
}
|
|||
|
|
|||
|
if (!(this instanceof OptionsValidator)) {
|
|||
|
return new OptionsValidator(params)
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = params.required
|
|||
|
|
|||
|
this.getRequiredOptions = function () {
|
|||
|
return requiredOptions
|
|||
|
}
|
|||
|
|
|||
|
this.validate = function (parameters) {
|
|||
|
const errors = []
|
|||
|
requiredOptions.forEach(function (requiredOptionName) {
|
|||
|
if (typeof parameters[requiredOptionName] === 'undefined') {
|
|||
|
errors.push(requiredOptionName)
|
|||
|
}
|
|||
|
})
|
|||
|
return errors
|
|||
|
}
|
|||
|
|
|||
|
function validateParams (params) {
|
|||
|
if (!params) {
|
|||
|
return false
|
|||
|
}
|
|||
|
return typeof params.required !== 'undefined' && params.required instanceof Array
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$utils_9 = {
|
|||
|
merge: merge,
|
|||
|
isJSON: isJSON
|
|||
|
}
|
|||
|
|
|||
|
function merge (defaultParams, mergeParams) {
|
|||
|
const mergedOptions = {}
|
|||
|
for (const option in defaultParams) {
|
|||
|
mergedOptions[option] = defaultParams[option]
|
|||
|
if (typeof mergeParams[option] !== 'undefined') {
|
|||
|
mergedOptions[option] = mergeParams[option]
|
|||
|
}
|
|||
|
}
|
|||
|
return mergedOptions
|
|||
|
}
|
|||
|
|
|||
|
function isJSON (json) {
|
|||
|
try {
|
|||
|
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
|
|||
|
return true
|
|||
|
}
|
|||
|
return false
|
|||
|
} catch (err) {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _$src_8 = {};
|
|||
|
(function (window) {
|
|||
|
'use strict'
|
|||
|
|
|||
|
let options = {
|
|||
|
searchInput: null,
|
|||
|
resultsContainer: null,
|
|||
|
json: [],
|
|||
|
success: Function.prototype,
|
|||
|
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
|
|||
|
templateMiddleware: Function.prototype,
|
|||
|
sortMiddleware: function () {
|
|||
|
return 0
|
|||
|
},
|
|||
|
noResultsText: 'No results found',
|
|||
|
limit: 10,
|
|||
|
fuzzy: false,
|
|||
|
debounceTime: null,
|
|||
|
exclude: []
|
|||
|
}
|
|||
|
|
|||
|
let debounceTimerHandle
|
|||
|
const debounce = function (func, delayMillis) {
|
|||
|
if (delayMillis) {
|
|||
|
clearTimeout(debounceTimerHandle)
|
|||
|
debounceTimerHandle = setTimeout(func, delayMillis)
|
|||
|
} else {
|
|||
|
func.call()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
|
|||
|
|
|||
|
/* removed: const _$Templater_7 = require('./Templater') */;
|
|||
|
/* removed: const _$Repository_4 = require('./Repository') */;
|
|||
|
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
|
|||
|
const optionsValidator = _$OptionsValidator_3({
|
|||
|
required: requiredOptions
|
|||
|
})
|
|||
|
/* removed: const _$utils_9 = require('./utils') */;
|
|||
|
|
|||
|
window.SimpleJekyllSearch = function (_options) {
|
|||
|
const errors = optionsValidator.validate(_options)
|
|||
|
if (errors.length > 0) {
|
|||
|
throwError('You must specify the following required options: ' + requiredOptions)
|
|||
|
}
|
|||
|
|
|||
|
options = _$utils_9.merge(options, _options)
|
|||
|
|
|||
|
_$Templater_7.setOptions({
|
|||
|
template: options.searchResultTemplate,
|
|||
|
middleware: options.templateMiddleware
|
|||
|
})
|
|||
|
|
|||
|
_$Repository_4.setOptions({
|
|||
|
fuzzy: options.fuzzy,
|
|||
|
limit: options.limit,
|
|||
|
sort: options.sortMiddleware,
|
|||
|
exclude: options.exclude
|
|||
|
})
|
|||
|
|
|||
|
if (_$utils_9.isJSON(options.json)) {
|
|||
|
initWithJSON(options.json)
|
|||
|
} else {
|
|||
|
initWithURL(options.json)
|
|||
|
}
|
|||
|
|
|||
|
const rv = {
|
|||
|
search: search
|
|||
|
}
|
|||
|
|
|||
|
typeof options.success === 'function' && options.success.call(rv)
|
|||
|
return rv
|
|||
|
}
|
|||
|
|
|||
|
function initWithJSON (json) {
|
|||
|
_$Repository_4.put(json)
|
|||
|
registerInput()
|
|||
|
}
|
|||
|
|
|||
|
function initWithURL (url) {
|
|||
|
_$JSONLoader_2.load(url, function (err, json) {
|
|||
|
if (err) {
|
|||
|
throwError('failed to get JSON (' + url + ')')
|
|||
|
}
|
|||
|
initWithJSON(json)
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function emptyResultsContainer () {
|
|||
|
options.resultsContainer.innerHTML = ''
|
|||
|
}
|
|||
|
|
|||
|
function appendToResultsContainer (text) {
|
|||
|
options.resultsContainer.innerHTML += text
|
|||
|
}
|
|||
|
|
|||
|
function registerInput () {
|
|||
|
options.searchInput.addEventListener('input', function (e) {
|
|||
|
if (isWhitelistedKey(e.which)) {
|
|||
|
emptyResultsContainer()
|
|||
|
debounce(function () { search(e.target.value) }, options.debounceTime)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function search (query) {
|
|||
|
if (isValidQuery(query)) {
|
|||
|
emptyResultsContainer()
|
|||
|
render(_$Repository_4.search(query), query)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function render (results, query) {
|
|||
|
const len = results.length
|
|||
|
if (len === 0) {
|
|||
|
return appendToResultsContainer(options.noResultsText)
|
|||
|
}
|
|||
|
for (let i = 0; i < len; i++) {
|
|||
|
results[i].query = query
|
|||
|
appendToResultsContainer(_$Templater_7.compile(results[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isValidQuery (query) {
|
|||
|
return query && query.length > 0
|
|||
|
}
|
|||
|
|
|||
|
function isWhitelistedKey (key) {
|
|||
|
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
|
|||
|
}
|
|||
|
|
|||
|
function throwError (message) {
|
|||
|
throw new Error('SimpleJekyllSearch --- ' + message)
|
|||
|
}
|
|||
|
})(window)
|
|||
|
|
|||
|
}());
|
|||
|
</script>
|
|||
|
|
|||
|
<!-- Configuration -->
|
|||
|
<script>
|
|||
|
SimpleJekyllSearch({
|
|||
|
searchInput: document.getElementById('search-input'),
|
|||
|
resultsContainer: document.getElementById('results-container'),
|
|||
|
json: '/search.json',
|
|||
|
//searchResultTemplate: '<li><a href="https://static.rnmkcy.eu{url}">{date} {title}</a></li>'
|
|||
|
searchResultTemplate: '<li><a href="{url}">{date} {title}</a></li>'
|
|||
|
})
|
|||
|
</script>
|
|||
|
<!-- Fin déclaration champ de recherche --></div><nav class="navigation">
|
|||
|
<ul><li class="navigation__item"><a href="/archive.html">Etiquettes</a></li><li class="navigation__item"><a href="/htmldoc.html">Documents</a></li><li class="navigation__item"><a href="/liens_ttrss.html">Liens</a></li><li class="navigation__item"><a href="/aide-jekyll-text-theme.html">Aide</a></li></ul>
|
|||
|
</nav></div>
|
|||
|
</header>
|
|||
|
|
|||
|
</div><div class="page__content"><div class ="main"><div class="grid grid--reverse">
|
|||
|
<div class="col-main cell cell--auto"><!-- start custom main top snippet --><div id="results-container" class="search-result js-search-result"></div><!-- end custom main top snippet -->
|
|||
|
<article itemscope itemtype="http://schema.org/Article"><div class="article__header"><header><h1 style="color:Tomato;">r-evolutions-dans-le-monde-de-la-sauvegarde-de-donnees</h1></header></div><meta itemprop="headline" content="r-evolutions-dans-le-monde-de-la-sauvegarde-de-donnees"><div class="article__info clearfix"><ul class="left-col menu"><li>
|
|||
|
<a class="button button--secondary button--pill button--sm"
|
|||
|
href="/archive.html?tag=divers">divers</a>
|
|||
|
</li></ul><ul class="right-col menu"><li>
|
|||
|
<i class="far fa-calendar-alt"></i> <span title="Création" style="color:#FF00FF">25 déc. 2019</span>
|
|||
|
|
|||
|
<span title="Modification" style="color:#00FF7F">23 nov. 2018</span></li></ul></div><meta itemprop="datePublished" content="2018-11-23T00:00:00+01:00">
|
|||
|
<meta itemprop="keywords" content="divers"><div class="js-article-content">
|
|||
|
<div class="layout--article"><!-- start custom article top snippet -->
|
|||
|
<style>
|
|||
|
#myBtn {
|
|||
|
display: none;
|
|||
|
position: fixed;
|
|||
|
bottom: 10px;
|
|||
|
right: 10px;
|
|||
|
z-index: 99;
|
|||
|
font-size: 12px;
|
|||
|
font-weight: bold;
|
|||
|
border: none;
|
|||
|
outline: none;
|
|||
|
background-color: white;
|
|||
|
color: black;
|
|||
|
cursor: pointer;
|
|||
|
padding: 5px;
|
|||
|
border-radius: 4px;
|
|||
|
}
|
|||
|
|
|||
|
#myBtn:hover {
|
|||
|
background-color: #555;
|
|||
|
}
|
|||
|
</style>
|
|||
|
|
|||
|
<button onclick="topFunction()" id="myBtn" title="Haut de page">⇧</button>
|
|||
|
|
|||
|
<script>
|
|||
|
//Get the button
|
|||
|
var mybutton = document.getElementById("myBtn");
|
|||
|
|
|||
|
// When the user scrolls down 20px from the top of the document, show the button
|
|||
|
window.onscroll = function() {scrollFunction()};
|
|||
|
|
|||
|
function scrollFunction() {
|
|||
|
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
|
|||
|
mybutton.style.display = "block";
|
|||
|
} else {
|
|||
|
mybutton.style.display = "none";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// When the user clicks on the button, scroll to the top of the document
|
|||
|
function topFunction() {
|
|||
|
document.body.scrollTop = 0;
|
|||
|
document.documentElement.scrollTop = 0;
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
|
|||
|
<!-- end custom article top snippet -->
|
|||
|
<div class="article__content" itemprop="articleBody"><details>
|
|||
|
<summary><b>Afficher/cacher Sommaire</b></summary>
|
|||
|
<!-- affichage sommaire -->
|
|||
|
<div class="toc-aside js-toc-root"></div>
|
|||
|
</details><p>URL: http://linuxfr.org/news/r-evolutions-dans-le-monde-de-la-sauvegarde-de-donnees
|
|||
|
Title: (R)évolutions dans le monde de la sauvegarde de données
|
|||
|
Authors: SaintGermain
|
|||
|
Nÿco, Benoît Sibaud, Benoît, Florent Zara, Nils Ratusznik, Maz, laurentb, Anthony F. et Zied
|
|||
|
Date: 2012-05-21T23:07:42+02:00
|
|||
|
License: CC by-sa
|
|||
|
Tags: sauvegarde, backup et déduplication
|
|||
|
Score: 89</p>
|
|||
|
|
|||
|
<p>Nous sommes presque deux mois après le <a href="http://www.worldbackupday.com">World Backup Day</a> mais il n’est jamais trop tard pour faire une sauvegarde. C’est une réalité qu’il faut accepter, votre <a href="http://www.hardware.fr/articles/862-6/disques-durs.html">disque dur va vous lâcher</a> et certainement <a href="http://fr.wikipedia.org/wiki/Loi_de_Murphy">au moment où cela vous embêtera le plus</a>. Et cela même si vous n’avez pas investi dans un superbe <a href="http://www.hardware.fr/articles/862-7/ssd.html">SSD OCZ</a> (jusqu’à 15,58% de taux de panne !).</p>
|
|||
|
|
|||
|
<p>Subir une défaillance d’un disque dur devient aussi de plus en plus grave à mesure qu’un nombre croissant de types de données se retrouve sous format numérique (musique, photos, messages, rapports, etc.), et que leur volume augmente.</p>
|
|||
|
|
|||
|
<p>C’est pourquoi le monde du logiciel (libre ou propriétaire) regorge de solutions pour sauvegarder vos précieuses données.</p>
|
|||
|
|
|||
|
<p>Cette dépêche (voir la seconde partie) n’abordera pas des solutions traditionnelles et éprouvées telles que le très connu <a href="http://www.amanda.org">Amanda</a>, le moins connu mais tout autant excellent <a href="http://dar.linux.free.fr">DAR</a> et les solutions « maison » à base de <a href="http://rsync.samba.org">rsync</a>. Elle n’abordera pas non plus des solutions trop restreintes à une plate-forme ou à un système de fichier (ZFS + snapshot par exemple). Elle va plutôt s’intéresser aux logiciels libres prenant en charge la <a href="http://fr.wikipedia.org/wiki/Déduplication">déduplication</a>.</p>
|
|||
|
|
|||
|
<p><img src="http://farm1.staticflickr.com/142/406866312_f3e6db02b5_n.jpg" alt="Backup Tape Malfunction" />
|
|||
|
Sous licence CC by-sa par <a href="http://www.flickr.com/photos/gemstone/406866312/">Mrs. Gemstone</a></p>
|
|||
|
|
|||
|
<hr />
|
|||
|
|
|||
|
<p><a href="https://github.com/apenwarr/bup">bup</a>
|
|||
|
<a href="http://burp.grke.net/">BURP</a>
|
|||
|
<a href="http://stromberg.dnsalias.org/~strombrg/backshift/">Backshift</a>
|
|||
|
<a href="http://liw.fi/obnam/">Obnam</a>
|
|||
|
<a href="http://www.hardware.fr/articles/862-6/disques-durs.html">Taux de retour des disques durs</a>
|
|||
|
<a href="http://www.hardware.fr/articles/862-7/ssd.html">Taux de retour des SSD</a>
|
|||
|
<a href="https://tahoe-lafs.org/trac/tahoe-lafs">tahoe-LAFS</a>
|
|||
|
<a href="http://backuppc.sourceforge.net/">BackupPC</a>
|
|||
|
<a href="http://stromberg.dnsalias.org/~strombrg/backshift/documentation/comparison/index.html">Comparatif effectué par l’auteur de Backshift</a></p>
|
|||
|
|
|||
|
<hr />
|
|||
|
|
|||
|
<h1 id="préliminaire">Préliminaire</h1>
|
|||
|
|
|||
|
<p>Voici tout d’abord quelques précisions sur des concepts importants pour la dépêche :</p>
|
|||
|
|
|||
|
<h2 id="client-serveur">Client-serveur</h2>
|
|||
|
<p>Tous les logiciels présentés peuvent être considérés d’un point de vue client-serveur même si pour la plupart des personnes, c’est en pratique la même machine. Le disque du client contient les données à sauvegarder et le disque du serveur contient les données sauvegardées.</p>
|
|||
|
|
|||
|
<p>Les différents logiciels à installer sur le client ou le serveur dépendent de la solution logicielle choisie.</p>
|
|||
|
|
|||
|
<h2 id="déduplication"><a href="http://fr.wikipedia.org/wiki/Déduplication">Déduplication</a></h2>
|
|||
|
<p>La déduplication permet d’économiser de l’espace disque en détectant les fichiers identiques, ou les morceaux (<em>chunk</em>) de fichiers identiques (pratique pour les <a href="http://fr.wikipedia.org/wiki/Machine_virtuelle">VM</a> dont seule une petite partie est modifiée).</p>
|
|||
|
|
|||
|
<p>La déduplication peut-être faite sur le serveur (par un programme dédié ou en utilisant un système de fichier adapté, par exemple <a href="http://www.lessfs.com/wordpress/">lessfs</a>, <a href="http://en.wikipedia.org/wiki/ZFS">ZFS</a> ou <a href="http://en.wikipedia.org/wiki/Btrfs">Btrfs</a>) ou sur le client (plus avantageux car optimise le trafic réseau). Dans le cas de multiples clients, il est aussi possible de dédupliquer entre eux.</p>
|
|||
|
|
|||
|
<p>Si la déduplication est bien faite, il n’y a plus besoin de notions de <a href="http://en.wikipedia.org/wiki/Delta_encoding"><em>delta encoding</em></a> et de <a href="http://fr.wikipedia.org/wiki/Sauvegarde#M.C3.A9thodes_.28Types.29_de_sauvegarde_les_plus_courantes">sauvegarde complète, différentielle et incrémentale</a>, et du coup c’est beaucoup plus simple à gérer.</p>
|
|||
|
|
|||
|
<p>Le désavantage est que si le morceau dédupliqué est perdu/corrompu, tout le monde est touché.
|
|||
|
Sans déduplication, s’il vous reste une sauvegarde complète vous n’avez pas tout perdu. Avec déduplication, il est facile de perdre beaucoup d’un coup.
|
|||
|
Il est donc primordial de bien gérer la redondance si vous vous orientez vers la déduplication.</p>
|
|||
|
|
|||
|
<h2 id="chiffrement">Chiffrement</h2>
|
|||
|
<p>La protection de la vie privée est pour certains « un problème de vieux » (voir <a href="http://www.lemonde.fr/technologies/article/2009/03/17/la-vie-privee-un-probleme-de-vieux-cons_1169203_651865.html">la-vie-privee-un-probleme-de-vieux-cons</a>, <a href="http://www.cenedella.com/job-search/privacy-is-for-old-people-says-linked-in-founder">privacy-is-for-old-people-says-linked-in-founder</a>, <a href="http://www.guardian.co.uk/technology/2010/jan/11/facebook-privacy">Privacy no longer a social norm</a>) mais pour d’autres c’est absolument essentiel.</p>
|
|||
|
|
|||
|
<p>Le cambriolage est toujours un risque (tremblez à l’idée de ce que <a href="http://www.theatlantic.com/magazine/archive/2011/11/hacked/8673/?single_page=true">quelqu’un pourrait faire avec tous vos emails</a>) et dans le cas d’un serveur dédié ou un disque virtuel loué à une compagnie, il faut faire confiance à cette compagnie (<a href="http://www.theverge.com/2012/4/25/2973849/google-drive-terms-privacy-data-skydrive-dropbox-icloud">google-drive-terms-privacy-data-skydrive-dropbox-icloud</a> et <a href="http://www.zdnet.com/blog/bott/sorry-dropbox-i-still-dont-trust-you/4173">sorry-dropbox-i-still-dont-trust-you</a>).</p>
|
|||
|
|
|||
|
<p>Le chiffrement est généralement fait sur le client, de manière “traditionnelle” en chiffrant les (morceaux de) fichiers avec par exemple <a href="http://www.gnupg.org/">GnuPG</a> ou bien en montant un système de fichier chiffré (<a href="http://fuse.sourceforge.net/sshfs.html">sshfs</a> + <a href="http://www.arg0.net/encfs">EncFS</a>).
|
|||
|
Dans le cas de sauvegardes non chiffrées, il est néanmoins possible de sécuriser la connexion (via <a href="http://fr.wikipedia.org/wiki/Transport_Layer_Security">SSL</a> par exemple) mais bon du coup vous chiffrez ET déchiffrez les données et au final elles sont stockées en clair.</p>
|
|||
|
|
|||
|
<p><em>Note : à noter que chiffrement et déduplication sont délicats à gérer ensemble (voir cette analyse sur le sujet de la <a href="http://security.stackexchange.com/questions/7142/online-backup-how-could-encryption-and-de-duplication-be-compatible">convergent encryption</a>).</em></p>
|
|||
|
|
|||
|
<p><em>Note 2 : je ne m’y connais pas trop, mais il me semble qu’avec un système de fichier chiffré on perd en souplesse (par exemple difficile d’isoler chaque utilisateur) par rapport à la manière “traditionnelle” mais que au niveau sécurité c’est kif-kif.</em></p>
|
|||
|
|
|||
|
<h2 id="redondance">Redondance</h2>
|
|||
|
<p>Quelques études récentes donnent des taux de panne intéressants :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li><a href="http://www.hardware.fr/articles/862-6/disques-durs.h">Taux de panne des disques durs</a></li>
|
|||
|
<li><a href="http://www.hardware.fr/articles/862-7/ssd.html">Taux de pannes des SSD</a></li>
|
|||
|
<li><a href="http://www.tomshardware.com/reviews/ssd-reliability-failure-rate,2923.html">Étude sur la fiabilité des SSD</a></li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p>Il est possible d’atténuer les conséquences d’une possible (voire probable) défaillance du support contenant vos sauvegardes.</p>
|
|||
|
|
|||
|
<p>Pour cela vous pouvez :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>diversifier vos supports de sauvegarde : par exemple avec un disque dur + un DVD de temps en temps (que vous stockerez idéalement chez quelqu’un d’autre) ou un disque chez vous et chez votre voisin. C’est une très bonne solution si vous réussissez à bien automatiser (dur dur pour un particulier).</li>
|
|||
|
<li>utiliser un système de fichier plus robuste : par exemple ZFS propose l’auto-réparation même si vous n’utilisez qu’un disque. Je ne connais pas trop les avantages/inconvénients de cette approche.</li>
|
|||
|
<li>utiliser du <a href="http://fr.wikipedia.org/wiki/RAID_%28informatique%29">RAID</a> (matériel ou logiciel) : intéressante si vous avez besoin d’une excellente disponibilité. La robustesse au cambriolage et au feu n’est pas bonne par contre. Un peu délicat et cher à mettre en place pour un particulier.</li>
|
|||
|
<li>ajouter des sommes de contrôle permettant l’auto-réparation : par exemple (<a href="http://parchive.sourceforge.net/">Parchive</a> ou <a href="https://tahoe-lafs.org/trac/zfec">zfec</a>). Vous pouvez configurer le taux de redondances (robustesse à 5% de corruption par exemple) et conserver ces sommes de contrôles ailleurs (à noter que je crois que les sommes de contrôles sont elles-même auto-réparables).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="multiplateforme"><a href="http://fr.wikipedia.org/wiki/Multiplate-forme">Multiplateforme</a></h2>
|
|||
|
<p>Si vous avez des plates-formes différentes, il est important que le logiciel prenne bien en compte les spécificités de la plate-forme. Sous les systèmes de type Unix, vous avez par exemple les <a href="http://fr.wikipedia.org/wiki/Access_Control_List">ACL</a>, les liens symboliques ou directs, les modes spéciaux (setuid, setgid, sticky), etc. Sous Windows, <a href="http://backuppc.sourceforge.net/faq/limitations.html#locked_files_are_not_backed_up">certains fichiers sont verrouillées lorsqu’ils sont utilisés</a> (par exemple les données d’Outlook) et il est alors recommandé d’utiliser la solution Microsoft <a href="http://en.wikipedia.org/wiki/Shadow_Copy">Volume Shadow Copy Service (VSS)</a>.</p>
|
|||
|
|
|||
|
<p><em>Note : Si l’on se restreint à un environnement particulier, il est possible d’avoir des solutions élégantes et performantes (comme ZFS + snapshot par exemple).</em></p>
|
|||
|
|
|||
|
<h2 id="importants-mais-non-traités">Importants mais non traités</h2>
|
|||
|
<p>Si vous voulez sauvegarder de manière pérenne sur de nombreuses années, il est important de aussi prendre en compte le format de stockage de vos données (i.e. est-ce que le format est standard ? est-il bien documenté ? dispose-t-il d’une API ?). Malheureusement par manque de temps, cette dépêche ne couvrira pas cet aspect.</p>
|
|||
|
|
|||
|
<p>La restauration reste normalement une opération beaucoup moins courante que la sauvegarde. Donc je n’ai pas trop cherché à voir si les logiciels offrent une interface évoluée pour parcourir et restaurer des données spécifiques. Du moment qu’il est possible (heureusement !) de restaurer un fichier, même de manière plus ou moins compliquée (ligne de commande tarabiscotée), je considère que le logiciel est acceptable. Malheureusement par manque de temps, je ne pourrai pas pousser le sujet plus loin.</p>
|
|||
|
|
|||
|
<h1 id="logiciels-présentés">Logiciels présentés</h1>
|
|||
|
|
|||
|
<ol>
|
|||
|
<li><strong>bup</strong> (0.25~git2011.11.04-3) : codé en Python/C, il s’appuie sur <a href="http://git-scm.com">Git</a>. Licence GNU LGPLv2.</li>
|
|||
|
<li><strong>BURP</strong> (1.3.8-1) : codé en C, il possède une architecture client/serveur et s’inspire de <a href="http://bacula.org">Bacula</a>. Pour Windows il utilise en particulier <a href="http://en.wikipedia.org/wiki/Shadow_Copy">Windows Volume Shadow Copy Service (VSS)</a>. Licence AGPLv3</li>
|
|||
|
<li><strong>Backshift</strong> (1.03) : codé en Python avec une architecture client/serveur, il a la particularité d’intégrer la déduplication à la source de pouvoir fonctionner simultanément (<a href="http://en.wikipedia.org/wiki/Concurrency_%28computer_science%29"><em>concurrency</em></a>). Licence principalement <a href="http://stromberg.dnsalias.org/~strombrg/backshift/documentation/licenses/index.html">GPLv3 avec des bouts UCI, Apache et MIT</a>.</li>
|
|||
|
<li><strong>Obnam</strong> (1.0) : codé en Python, c’est à ce jour le seul logiciel libre intégrant à la fois la déduplication des morceaux de fichiers sur le client et le chiffrement “traditionnel” (avec <a href="http://www.gnupg.org">GnuPG</a>). Licence GPLv3 ou postérieure.</li>
|
|||
|
<li><strong>tahoe-LAFS</strong> (1.9.1) : codé en Python (mais inclut des bibliothèques tierces codées en C/C++), c’est un système de stockage redondant décentralisé (“dans le nuage”) avec une forte composante vie privée/sécurité/cryptographie. Licence GPLv2 ou postérieure, <a href="https://tahoe-lafs.org/trac/tahoe-lafs/browser/COPYING.TGPPL.rst">TGPPLv1</a> ou postérieure.</li>
|
|||
|
<li><strong>BackupPC</strong> (3.2.1-3) : vénérable logiciel de sauvegarde (depuis 2001), codé en Perl et C. Apprécié des administrateurs de parc de machines hétérogènes, il s’appuie sur <a href="http://rsync.samba.org">rsync</a> sur Linux, Unix ou Mac OS X et/ou sur le <a href="http://fr.wikipedia.org/wiki/Server_Message_Block">protocole SMB</a> sur Windows. Licence GPLv2 ou postérieure.</li>
|
|||
|
</ol>
|
|||
|
|
|||
|
<p><em>Avertissement : les logiciels (hormis BackupPC) n’étant pas tout à fait mûrs sur certains points, je pense personnellement que c’est pour le moment à réserver aux curieux aventuriers et pas à ceux qui souhaitent en faire leur solution principale de sauvegarde clé en main sans se prendre la tête. Pour info pendant l’évaluation, j’ai trouvé un <a href="http://groups.google.com/group/bup-list/msg/69cbd41a63edc60a">bogue</a> et un <a href="http://groups.google.com/group/bup-list/msg/b53f202484c96a97">problème de performance</a> sur la dernière version de bup, signalé un <a href="http://vlists.pepperfish.net/pipermail/obnam-flarn.net/2012-June/000066.html">problème 1 jour avant la sortie de la version 1.0 de Obnam</a>, eu quelques <a href="http://groups.google.com/group/backshift/browse_thread/thread/60915d2e01fd675f">problèmes de performances avec la dernière version de Backshift</a> et <a href="http://sourceforge.net/mailarchive/message.php?msg_id=29373712">BURP a un problème avec l’avant-dernière version (1.3.6)</a>.</em></p>
|
|||
|
|
|||
|
<p><em>Note : ça fait plaisir de voir que Python est autant utilisé ;-)</em></p>
|
|||
|
|
|||
|
<h1 id="contexte">Contexte</h1>
|
|||
|
|
|||
|
<p>Tout d’abord précisons pourquoi à titre personnel je me suis intéressé à ces logiciels. Tout allait plus ou moins bien sur mon ordinateur portable au niveau des sauvegardes. Je les enregistrais sur un (gros) disque externe au moyen de <a href="http://dar.linux.free.fr">DAR</a> et comble du luxe, j’ajoutais même des sommes de contrôles <a href="http://parchive.sourceforge.net">PAR2</a> pour rendre mes archives auto-réparables (jusqu’à 5% de corruption).</p>
|
|||
|
|
|||
|
<p>Cependant, tout n’est pas rose car :</p>
|
|||
|
|
|||
|
<ol>
|
|||
|
<li>Un (gros) disque externe ce n’est pas très pratique. Et donc en pratique je ne le branchais pas souvent et mes sauvegardes n’étaient ni fréquentes, ni régulières.</li>
|
|||
|
<li>Un disque externe, ce n’est pas très robuste aux cambriolages et au feu.</li>
|
|||
|
<li>Je suis un peu bordélique sur les bords, donc il y beaucoup de fichiers qui sont rangés un peu n’importe comment et je range toutes les deux éclipses totales. <a href="http://dar.linux.free.fr">DAR</a>, comme beaucoup d’autres logiciels similaires, détecte un fichier qui a bougé comme un nouveau fichier, donc les sauvegardes incrémentales prennent beaucoup de place.</li>
|
|||
|
<li>Je commence à utiliser des machines virtuelles qui sont très grosses (de l’ordre de 1 Go, voire plus). Au moindre petit changement dans ces machines virtuelles, il faut tout ré-enregistrer.</li>
|
|||
|
<li>J’aimerais bien aussi faire quelque chose pour la famille et les amis qui sont un peu novices en informatique et qui sont sous Windows ou Mac OS.</li>
|
|||
|
</ol>
|
|||
|
|
|||
|
<p>J’ai donc commencé à m’intéresser aux serveurs dédiés (<a href="http://www.kimsufi.com/fr">Kimsufi</a> proposant 1 To pour 216 € par an, cela commence à être intéressant par rapport à de l’auto-hébergement moins robuste aux cambriolages et au feu) et aux logiciels libres de sauvegarde efficaces sur la sauvegarde à distance (i.e. optimisant les échanges et la bande-passante et robuste à une déconnexion).
|
|||
|
C’est plus cher et beaucoup plus fastidieux à mettre en place, mais c’est à mettre en balance avec la valeur que vous accordez à vos données.</p>
|
|||
|
|
|||
|
<p>Un contre-point intéressant serait de comparer avec une personne qui aurait tous ses emails sous Gmail, ses photos sous Flickr, ses documents sous Dropbox, sa musique avec Deezer et sa vie numérique sous Facebook. Il est plus difficile dans ce cas d’expliquer la nécessité de la sauvegarde (défaillance du fournisseur, capture du consommateur), sans même parler de vie privée.</p>
|
|||
|
|
|||
|
<p><em>Note : même si cela n’est pas exactement le même sujet, j’ai été aussi impressionné par la facilité d’utilisation et les performances de Dropbox. Il combine la synchronisation des données ainsi que le versioning des fichier (on peut presque dire que c’est plus ou moins équivalent à de la sauvegarde) de manière très intuitive. Voir cette <a href="http://linuxfr.org/news/synchroniser-vos-dossiers-locaux-distants">dépêche</a> présentant quelques équivalents libres (et qui m’a fait découvrir bup et Obnam).</em></p>
|
|||
|
|
|||
|
<h1 id="bup">bup</h1>
|
|||
|
|
|||
|
<p>Approche originale fondée sur le fameux <a href="http://git-scm.com/">Git</a> et utilise en particulier ses <a href="http://alblue.bandlem.com/2011/09/git-tip-of-week-packfiles-redux.html">packfiles</a> :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Si vous avez de nombreux petits fichiers, les performances devraient donc être très intéressantes. La <a href="http://fr.wikipedia.org/wiki/Déduplication">déduplication</a> est le gros point fort de <strong>bup</strong> et c’est peut-être le logiciel optimisant le mieux l’espace disque et le trafic réseau pour vos sauvegardes (déduplication à la source et ne transmet que les changements/deltas).</li>
|
|||
|
<li>Peut générer les sommes de contrôles <a href="http://parchive.sourceforge.net">PAR2</a> pour vos sauvegardes, afin de réparer les corruptions éventuelles.</li>
|
|||
|
<li>Fonctionne avec Linux, Mac OS X >= 10.4, Solaris, ou Windows (avec Cygwin). J’ai quelques doutes pour les fichiers verrouillés sous Windows.</li>
|
|||
|
<li>Ne gère pas encore officiellement les metadatas des fichiers (les patchs sont prêts mais il reste quelques bogues).</li>
|
|||
|
<li>Ne gère pas l’expiration ou la rétention des données.</li>
|
|||
|
<li>Pas de chiffrement des données.</li>
|
|||
|
<li>La liste de diffusion et la communauté semblent actives mais moins dynamiques qu’auparavant. La dernière version officielle de bup sous Debian Unstable remonte au 04/11/11. Pas facile de trouver où est le <a href="http://git.debian.org/?p=users/rlb/bup.git">dépôt contenant les sources les plus à jour</a>. Sur les problèmes que j’ai remontés, je n’ai pour l’instant pas eu beaucoup d’aide.</li>
|
|||
|
<li>Pas de bugtracker</li>
|
|||
|
<li>Comme il s’appuie sur Git, on peut s’attendre à une certaine robustesse. Le format de l’archive est de plus compatible avec Git.</li>
|
|||
|
<li>Robustesse à l’interruption de la sauvegarde</li>
|
|||
|
<li>Fonctionnement en ligne de commande.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="burp">BURP</h1>
|
|||
|
|
|||
|
<p><strong>BURP</strong> (oui le nom peut prêter à confusion avec le précédent) s’inspire donc de <a href="http://www.bacula.org">Bacula</a> (<a href="http://burp.grke.net/why.html">l’auteur explique pourquoi</a>) et possède de nombreuses fonctionnalités intéressantes :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Architecture client-serveur facilement configurable (pratique pour gérer les sauvegardes de la famille)</li>
|
|||
|
<li>Intègre un mélange de <a href="http://en.wikipedia.org/wiki/Delta_encoding">delta encoding</a> pour n’envoyer que la partie modifiée d’un fichier (pratique pour les VM) et de déduplication de fichiers <em>au niveau du serveur</em> (et à lancer manuellement avec bedup).</li>
|
|||
|
<li>Gère le chiffrement symétrique Blowfish des sauvegardes (mais cela désactive le <a href="http://en.wikipedia.org/wiki/Delta_encoding">delta encoding</a>). Cependant le nom des fichiers n’est pas chiffré (et la taille du fichier chiffré étant similaire, vous dévoilez une partie non négligeable de l’information).</li>
|
|||
|
<li>Très bonne prise en charge des aspects multiplateformes. Utilise VSS pour les sauvegardes Windows : cela permet de gérer les problèmes de <a href="http://backuppc.sourceforge.net/faq/limitations.html#locked_files_are_not_backed_up">verrouillage de certains fichiers</a></li>
|
|||
|
<li>Bien adapté pour des sauvegardes à distance : reprise de la sauvegarde en cas d’interruption (le fichier en cours n’a pas besoin d’être retransmis dans son intégralité), génération automatique de certificats SSL, utilisation de SSL obligatoire pour toute communication.</li>
|
|||
|
<li>Le serveur fonctionne uniquement sur Unix. Client Unix et Windows disponible.</li>
|
|||
|
<li>De par son fonctionnement (reverse deltas : la dernière sauvegarde contient la dernière version des fichiers et les sauvegardes précédentes sont modifiées pour contenir uniquement le delta), je ne pense pas que cela soit possible d’implémenter un mécanisme <a href="http://parchive.sourceforge.net/">Parchive</a> ou <a href="https://tahoe-lafs.org/trac/zfec">zfec</a> (sauf à désactiver le delta encoding).</li>
|
|||
|
<li>Planification évoluée des sauvegardes</li>
|
|||
|
<li>Nombreuses options pour la rétention/expiration des sauvegardes</li>
|
|||
|
<li>Mise à jour automatique des clients</li>
|
|||
|
<li>l’auteur est très sympa et très réactif (parfois < 5 minutes !) sur la liste de diffusion. Par contre il semble être le seul à développer.</li>
|
|||
|
<li>Fonctionnement en ligne de commande.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="tahoe-lafs">tahoe-LAFS</h1>
|
|||
|
|
|||
|
<p><strong>tahoe-LAFS</strong> n’est pas seulement un logiciel de sauvegarde : c’est un système de fichiers distribué qui intègre <em>en plus</em> un outil de sauvegarde. L’objectif premier est de sécuriser vos données, aussi bien du point de vue vie privée que du point de vue perte de données. Pour cela il stocke vos données chiffrées (à la source) sur plusieurs machines organisées en réseau avec une politique configurable (si vous spécifiez K=2 et N=5, vos données seront réparties sur 5 machines, dont au moins 2 doivent être disponibles pour accéder à vos données).</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>J’aime bien son approche “paranoïaque”. L’idée directrice est que personne (pas même l’hébergeur qui détient la machine contenant la sauvegarde) ne devrait pouvoir accéder en lecture/écriture à vos données.</li>
|
|||
|
<li>Le besoin de sommes de contrôle <a href="http://parchive.sourceforge.net/">Parchive</a> ou <a href="https://tahoe-lafs.org/trac/zfec">zfec</a> est moins présent vu que les données sont dupliquées sur le réseau (je recommande quand même une petite sauvegarde de temps en temps sur un autre média au cas où)</li>
|
|||
|
<li>La communauté et la liste de diffusion sont très actives et très sympathiques (postez un message sur la liste de diffusion pour voir !). La documentation est de qualité.</li>
|
|||
|
<li>Vous pouvez rejoindre un réseau existant (comme <a href="http://bigpig.org/">VolunteerGrid2</a>) en ajoutant votre machine. Du coup tous les membres du réseau auront accès à une partie de vos données (chiffrées !) et vice-versa. C’est un saut psychologique important à faire (il faut faire confiance au chiffrement)</li>
|
|||
|
<li>Vous pouvez aussi louer de l’espace à <a href="https://leastauthority.com/">Least Authority Enterprises</a> ou un réseau privé entier à <a href="https://www.rentanode.nl">RentaNode</a>.</li>
|
|||
|
<li>Pas de <a href="http://en.wikipedia.org/wiki/Delta_encoding">delta encoding</a> mais intègre la déduplication à la source mais seulement au niveau d’un fichier entier.</li>
|
|||
|
<li>Fonctionne sur tous les systèmes suffisamment conformes à la norme <a href="http://fr.wikipedia.org/wiki/POSIX">POSIX</a> (linux, *BSD, Mac OS X, Windows, etc.). J’ai quelques doutes pour les fichiers verrouillés sous Windows. Mais il est possible d’utiliser <a href="http://www.duplicati.com/">Duplicati</a> pour gérer les sauvegardes avec Windows VSS.</li>
|
|||
|
<li>En cas d’interruption d’une sauvegarde, le fichier en cours doit être retransmis dans son intégralité.</li>
|
|||
|
<li>Chiffrement avec AES-128 (bientôt <a href="https://tahoe-lafs.org/trac/tahoe-lafs/ticket/1164">combiné avec XSalsa20</a>)</li>
|
|||
|
<li>Fonctionnement en ligne de commande, via votre navigateur et FTP/SFTP.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="obnam">Obnam</h1>
|
|||
|
|
|||
|
<p><strong>Obnam</strong> s’inspire de <a href="http://fr.wikipedia.org/wiki/Btrfs">Btrfs</a> en utilisant les B-tree (<a href="http://fr.wikipedia.org/wiki/Copy-On-Write">copie sur écriture</a>). La version 1.0 vient de sortir la semaine dernière. C’est (à ma connaissance) le seul logiciel libre intégrant le chiffrement et la déduplication (sur le client) des morceaux de fichiers.</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Déduplication à la source des morceaux de fichier (“file data chunks”) donc pas besoin de delta encoding (mais perte légère d’efficacité qui dépend de la taille des morceaux). L’algorithme est cependant moins performant que celui de bup car il gère mal le décalage à l’intérieur d’un fichier (voir cet <a href="http://vlists.pepperfish.net/pipermail/obnam-flarn.net/2012-June/000073.html">exemple de suppression d’une ligne</a>).</li>
|
|||
|
<li><a href="http://liw.fi/obnam/locking/">Déduplication possible entre plusieurs utilisateurs</a> avec chiffrement. Cela est possible en ayant en commun la même clé de chiffrement symétrique qui est elle-même chiffrée avec chacune des clefs publiques des utilisateurs. On peut ainsi révoquer un utilisateur (mais si celui-ci a copié la clef symétrique, c’est mort) et créer des groupes de personnes de confiance pour partager le même dépôt.</li>
|
|||
|
<li>Ne semble pas adapté à une sauvegarde à distance si vous avez beaucoup de petits fichiers (<a href="https://groups.google.com/forum/#!topic/bup-list/QNYW_VsaJqg">latence dans la commande SFTP RTT</a>)</li>
|
|||
|
<li><a href="http://fr.wikipedia.org/wiki/Cryptographie_hybride">Chiffrement hybride</a> des données avec GnuPG à la source. A noter que la clé privée est nécessaire pendant toute la durée de la sauvegarde car Obnam a besoin de déchiffrer des informations (“file data chunks checksums”) sur les sauvegardes précédentes pour effectuer la déduplication.</li>
|
|||
|
<li>Sécurisation de la connexion par <a href="http://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol">SSH FTP</a> (mais autant chiffrer de mon point de vue).</li>
|
|||
|
<li>Reprise possible d’une sauvegarde interrompue (placement de checkpoints après une taille configurable, on reprend alors à partir du checkpoint).</li>
|
|||
|
<li>Ne fonctionne que sur linux.</li>
|
|||
|
<li>Développé par une seule personne (un ancien copain de Linus !). Il répond généralement assez vite sur la liste de diffusion.</li>
|
|||
|
<li>Fonctionnement en ligne de commande.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="backuppc">BackupPC</h1>
|
|||
|
|
|||
|
<p><strong>BackupPC</strong> est un logiciel relativement connu car délivrant de bons et loyaux services depuis 2001. Il semble particulièrement adapté à la gestion de sauvegardes d’un parc de machines (outils d’administration à distance évolués) et devrait donc plaire aux administrateurs (personnellement en tant que particulier j’ai eu un peu de mal à lancer ma première sauvegarde). Il rentre dans la catégorie des logiciels de sauvegardes traditionnels et éprouvés, sauf qu’il tient tête aux petits nouveaux en intégrant lui-aussi la déduplication.</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Pas de chiffrement des données mais sécurisation de la connexion par SSL.</li>
|
|||
|
<li>Fonctionnement “classique” à base de sauvegardes complètes et incrémentales.</li>
|
|||
|
<li>Déduplication au niveau d’un fichier sur le serveur. Aussi possible entre plusieurs utilisateurs.</li>
|
|||
|
<li>Au moins sur Debian, il y a une dépendance sur Apache (!) car il intègre un outil web d’administration à distance (très bien fait d’ailleurs).</li>
|
|||
|
<li>Sous Windows, possibilité de sauvegarder les fichiers verrouillés pas très claire : voir la <a href="http://backuppc.sourceforge.net/faq/limitations.html#locked_files_are_not_backed_up">FAQ</a>, ce <a href="http://www.mail-archive.com/backuppc-users@lists.sourceforge.net/msg20989.html">message</a> ainsi que <a href="http://www.backupcentral.com/phpBB2/two-way-mirrors-of-external-mailing-lists-3/backuppc-21/rsync-3-0-x-with-windows-vss-support-98590/">celui-ci</a>.</li>
|
|||
|
<li>Sur linux, vous avez le choix entre tar et rsync. Les performances ne sont pas toujours similaires (voir par exemple cette <a href="http://adsm.org/lists/html/BackupPC-users/2011-02/msg00215.html">comparaison</a>).</li>
|
|||
|
<li>L’évolution est relativement lente (signe de maturité ?) : 3.2.0 le 02/08/10 et 3.2.1 le 09/05/11.</li>
|
|||
|
<li>Fonctionne sans installer de logiciels sur le client.</li>
|
|||
|
<li>Reprise possible d’une sauvegarde interrompue (Partial Backup).</li>
|
|||
|
<li>Documentation assez complète et de nombreuses ressources à disposition.</li>
|
|||
|
<li>Le serveur fonctionne avec Linux, Freenix, Solaris. Le client fonctionne avec Linux, Win95, Win98, Win2000 et WinXP.</li>
|
|||
|
<li>Fonctionne en ligne de commande et via votre navigateur (sous celui-ci, l’interface est extrêmement bien faite).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h1 id="backshift">Backshift</h1>
|
|||
|
|
|||
|
<p><strong>Backshift</strong> est visiblement développé par un amateur de Python (voir les <a href="http://stromberg.dnsalias.org/~strombrg/backshift/documentation/interpreters/index.h">différents interpréteurs testés</a> ainsi que les <a href="http://stromberg.dnsalias.org/~strombrg/backshift/documentation/performance/index.html">tests de performance</a>). L’accent est apparemment mis sur l’optimisation de la taille de l’archive (déduplication des morceaux de fichiers et utilisation de LZMA pour la compression)</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Déduplication des morceaux de fichiers sur le client.</li>
|
|||
|
<li>Possibilité de fonctionnement en parallèle de plusieurs clients avec déduplication entre eux (!).</li>
|
|||
|
<li>Reprise possible d’une sauvegarde interrompue.</li>
|
|||
|
<li>Pas de chiffrement des données.</li>
|
|||
|
<li>Sécurisation de la connexion possible par sshfs.</li>
|
|||
|
<li>Création initiale de nombreux répertoires/fichiers mais permettant en théorie de mieux optimiser par la suite le nombre de répertoires/fichiers sur de nombreuses sauvegardes.</li>
|
|||
|
<li>Fonctionnement en ligne de commande.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p>Je n’ai malheureusement pas réussi à avoir des performances suffisantes pour qu’il puisse rentrer convenablement dans les tests de performance. L’auteur est en train de voir mais cela peut prendre du temps. Je surveille en tout cas ce logiciel de près.</p>
|
|||
|
|
|||
|
<h1 id="tests-de-performance">Tests de performance</h1>
|
|||
|
|
|||
|
<p>Afin d’avoir une idée des performances de chacun des logiciels, j’ai créé un petit programme générant des nombres pseudo-aléatoires (j’ai limité aux nombres afin de bien voir l’effet de la compression).
|
|||
|
Pour chaque logiciel j’ai alors d’abord sauvegardé, puis modifié, puis re-sauvegardé et enfin effectué une restauration.</p>
|
|||
|
|
|||
|
<p>L’arborescence “Sauvegarde initiale” <a href="http://groups.google.com/group/backshift/msg/d3d645e4a5a55ae0">est la suivante</a> :</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>10 répertoires contenant 10 répertoires contenant 10 répertoires contenant 10 fichiers de 1 Mo (soit 10 000 fichiers)</li>
|
|||
|
<li>10 répertoires contenant 1 fichiers d’environ 500 Mo (soit 10 fichiers)</li>
|
|||
|
<li>3 répertoires contenant 1 fichiers d’environ 2 Go (soit 3 fichiers)
|
|||
|
Au final nous avons donc environ 10 000 fichiers et 24 Go.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p>Ensuite j’ai modifié l’arborescence (“Modifications”) en touchant 3 Go de fichiers :</p>
|
|||
|
|
|||
|
<ol>
|
|||
|
<li>Renommage d’un répertoire de petits fichiers (soit 1000 fichiers de 1 Mo)</li>
|
|||
|
<li>Renommage d’un fichier de 500 Mo</li>
|
|||
|
<li>Modification d’un fichier de 500 Mo (suppression d’une ligne au milieu)</li>
|
|||
|
<li>Modification d’un fichier de 500 Mo (modification d’une ligne au milieu)</li>
|
|||
|
<li>Duplication d’un fichier de 500 Mo</li>
|
|||
|
<li>Duplication d’un fichier de 500 Mo et modification d’une ligne au milieu</li>
|
|||
|
</ol>
|
|||
|
|
|||
|
<p>La “Restauration” est tout simplement une opération de restauration de toute l’arborescence.</p>
|
|||
|
|
|||
|
<p>J’ai ajouté un test “Machine Virtuelle” sur la sauvegarde d’une machine virtuelle avant et après installation de divers logiciels (la taille de la VM est de 4.6 Go avant, 7.3 Go après).</p>
|
|||
|
|
|||
|
<p>Les arborescences ainsi que les dépôts pour la sauvegarde sont sur le même disque dur interne de 5400 rpm (je n’en ai pas d’autres sous la main). Les tests sont réalisés sous Debian Testing (kernel 3.2.0-2-686-pa).</p>
|
|||
|
|
|||
|
<p><em>Note : sur un serveur de sauvegarde distant, les résultats devraient être encore plus marqués. On pourra de plus contrôler le volume de données qui transitent et les effets de latence. Des volontaires ?</em></p>
|
|||
|
|
|||
|
<h2 id="résultats-sauvegarde-initiale">Résultats “Sauvegarde initiale”</h2>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li><strong>bup</strong>¹ : 12693 Mo en 36mn28</li>
|
|||
|
<li><strong>BURP</strong>² : 11427 Mo en 79mn47</li>
|
|||
|
<li><strong>BURP</strong>² (avec chiffrement) : 11427 Mo en 80mn36</li>
|
|||
|
<li><strong>Obnam</strong> : 11607 Mo en 59mn55</li>
|
|||
|
<li><strong>Obnam</strong> (avec chiffrement) : 11697 Mo en 78mn43</li>
|
|||
|
<li><strong>BackupPC</strong> (avec tar) : 11729 Mo en 36mn24</li>
|
|||
|
<li><strong>BackupPC</strong> (avec rsync) : 11729 Mo en 38mn42</li>
|
|||
|
<li><strong>tahoe-LAFS</strong>³ : 24795 Mo en 110mn46</li>
|
|||
|
<li><strong>Backshift</strong>⁴ : j’ai arrêté la sauvegarde après 8h et 2000 fichiers traités.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p><em>Note 1 : La version de bup utilisée ne stocke pas les metadatas.</em></p>
|
|||
|
|
|||
|
<p><em>Note 2 : J’ai désactivé SSL pour BackupPC mais je n’ai pas réussi à l’enlever pour BURP, celui-ci est donc pénalisé.</em></p>
|
|||
|
|
|||
|
<p><em>Note 3 : tahoe-LAFS est configuré pour un seul noeud de stockage en local (K=H=N=1).</em></p>
|
|||
|
|
|||
|
<p><em>Note 4 : j’ai fait ce que j’ai pu pour accélérer Backshift en choisissant l’interpréteur Pypy (1.7), un disque externe formaté en ext4 (car Backshift crée beaucoup de répertoires/fichiers et en ext3 cela ne passe pas) et désactivé LZMA au profit de bzip2.</em></p>
|
|||
|
|
|||
|
<h2 id="résultats-modifications">Résultats “Modifications”</h2>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li><strong>bup</strong> : +37 Mo en 12mn43. C’est le roi de la déduplication, rien ne lui échappe.</li>
|
|||
|
<li><strong>BURP</strong> : +271 Mo en 18mn10. Les modifications 3 et 4 sont bien gérées (il n’envoie et stocke que le delta). 1, 2 et 5 sont gérées mais à postériori (déduplication sur le serveur), donc les données sont quand mêmes envoyées. 6 n’est pas gérée.</li>
|
|||
|
<li><strong>BURP</strong> (avec chiffrement) : +811 Mo en 25mn10. Idem que précedemment sauf que les modifications 3 et 4 ne sont plus gérées à cause du chiffrement.</li>
|
|||
|
<li><strong>Obnam</strong> : +140 Mo en 2mn40. La modification 3 est <a href="http://vlists.pepperfish.net/pipermail/obnam-flarn.net/2012-June/000073.html">partiellement gérée</a>.</li>
|
|||
|
<li><strong>Obnam</strong> (avec chiffrement) : +141 Mo en 3mn34. Identique au cas précédent.</li>
|
|||
|
<li><strong>BackupPC</strong> (avec tar) : +837 Mo en 26mn36. Les modifications 1, 2 et 5 sont gérées à postériori (déduplication sur le serveur), donc les données sont quand mêmes envoyées. 3, 4 et 6 ne sont pas gérées.</li>
|
|||
|
<li><strong>BackupPC</strong> (avec rsync) : +837 Mo en 6mn54. Identique au cas précédent.</li>
|
|||
|
<li><strong>tahoe-LAFS</strong> : +1751 Mo en 118mn34. Les modifications 1, 2 et 5 sont bien gérées. 3, 4 et 6 ne sont pas gérées.</li>
|
|||
|
<li><strong>Backshift</strong> : non testé.</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="résultats-restauration">Résultats “Restauration”</h2>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li><strong>bup</strong> : 33mn48</li>
|
|||
|
<li><strong>BURP</strong> : 27mn13</li>
|
|||
|
<li><strong>BURP</strong> (avec chiffrement) : 27mn</li>
|
|||
|
<li><strong>Obnam</strong> : 31mn10</li>
|
|||
|
<li><strong>Obnam</strong> (avec chiffrement) : 49mn</li>
|
|||
|
<li><strong>BackupPC</strong> (avec tar) : 22mn06</li>
|
|||
|
<li><strong>BackupPC</strong> (avec rsync) : 22mn24</li>
|
|||
|
<li><strong>tahoe-LAFS</strong> : 68mn20</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<h2 id="résultats-machine-virtuelle">Résultats “Machine Virtuelle”</h2>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li><strong>bup</strong> : 1955 Mo en 4mn03, puis +1717 Mo en 4mn42</li>
|
|||
|
<li><strong>BURP</strong> : 2640 Mo en 9mn35, puis +2060 Mo en 15mn23</li>
|
|||
|
<li><strong>Obnam</strong> : 2671 Mo en 6mn07, puis +3098 Mo en 9mn13</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p><em>Note : à noter qu’on est ici dans un cas similaire aux modifications 3 et 4. Cependant la sauvegarde de Machines Virtuelles se fait usuellement via des instantanés (“snapshots”) qui s’apparentent donc plutôt à la modification 6 (BURP ne pourra donc pas économiser quoi que ce soit).</em></p>
|
|||
|
|
|||
|
<h1 id="conclusion">Conclusion</h1>
|
|||
|
|
|||
|
<p>En conclusion voici mon avis <strong>personnel</strong> basé sur l’état <em>actuel</em> des logiciels. Encore une fois, pour une chose aussi sensible que la sauvegarde de vos données, je vous recommande de faire attention vu la maturité relative de ces logiciels (hormis BackupPC). Mais ils sont tous en plein développement, donc c’est le moment idéal pour y participer en les testant, en remontant les bogues ou en proposant un patch !</p>
|
|||
|
|
|||
|
<ul>
|
|||
|
<li>Si vous êtes sous un système de type Unix :
|
|||
|
<ul>
|
|||
|
<li>Si vous recherchez les performances pures (rapidité et optimisation de l’espace), bup est LA référence. Cependant ses limitations ‘opérationnelles’ actuelles (pas de gestion des metadatas, pas d’expiration des sauvegardes) peuvent être trop limitantes.</li>
|
|||
|
<li>Si vous voulez la performance ainsi que le chiffrement complet de vos données, Obnam est actuellement le meilleur choix (avec une réserve sur la sauvegarde à distance cependant).</li>
|
|||
|
</ul>
|
|||
|
</li>
|
|||
|
<li>Si vous êtes sous Windows :
|
|||
|
<ul>
|
|||
|
<li>BURP gère normalement le mieux les spécificités de Windows (pas essayé malheureusement) tout en maintenant un bon niveau de sécurité.</li>
|
|||
|
</ul>
|
|||
|
</li>
|
|||
|
<li>Si vous gérez un parc de machines hétérogènes :
|
|||
|
<ul>
|
|||
|
<li>Pour un particulier, BURP est peut-être plus accessible.</li>
|
|||
|
<li>Pour un administrateur, difficile de ne pas trouver son bonheur dans BackupPC qui est de plus mature et éprouvé.</li>
|
|||
|
</ul>
|
|||
|
</li>
|
|||
|
<li>Si vous êtes pressés et que vous voulez un logiciel prêt à l’emploi et disposant de toutes les fonctionnalités, je vous recommande chaudement BURP.</li>
|
|||
|
<li>Si vous très soucieux de votre vie privée, de la sécurité ainsi que de la redondance, tahoe-LAFS est fait pour vous (mais encore une fois ce n’est pas uniquement pour de la sauvegarde, c’est un système de fichiers distribué).</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<p><em>Merci à Nÿco, Benoît Sibaud, Florent Zara, Maz, Anthony F., ziedabid, Laurent Bachelier, Benoît et Beurt d’avoir lu et corrigé les fautes dans la dépêche.</em></p>
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
|
|||
|
|
|||
|
<div class="d-print-none"><footer class="article__footer"><meta itemprop="dateModified" content="2019-12-25T00:00:00+01:00"><!-- start custom article footer snippet -->
|
|||
|
|
|||
|
<!-- end custom article footer snippet -->
|
|||
|
<!--
|
|||
|
<div align="right"><a type="application/rss+xml" href="/feed.xml" title="S'abonner"><i class="fa fa-rss fa-2x"></i></a>
|
|||
|
|
|||
|
 </div>
|
|||
|
-->
|
|||
|
</footer>
|
|||
|
<div class="article__section-navigator clearfix"><div class="previous"><span>PRÉCÉDENT</span><a href="/2019/12/25/pyzor.html">pyzor</a></div><div class="next"><span>SUIVANT</span><a href="/2019/12/25/rainloop.html">rainloop</a></div></div></div>
|
|||
|
|
|||
|
</div>
|
|||
|
|
|||
|
<script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
$(function() {
|
|||
|
var $this ,$scroll;
|
|||
|
var $articleContent = $('.js-article-content');
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
var scroll = hasSidebar ? '.js-page-main' : 'html, body';
|
|||
|
$scroll = $(scroll);
|
|||
|
|
|||
|
$articleContent.find('.highlight').each(function() {
|
|||
|
$this = $(this);
|
|||
|
$this.attr('data-lang', $this.find('code').attr('data-lang'));
|
|||
|
});
|
|||
|
$articleContent.find('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]').each(function() {
|
|||
|
$this = $(this);
|
|||
|
$this.append($('<a class="anchor d-print-none" aria-hidden="true"></a>').html('<i class="fas fa-anchor"></i>'));
|
|||
|
});
|
|||
|
$articleContent.on('click', '.anchor', function() {
|
|||
|
$scroll.scrollToAnchor('#' + $(this).parent().attr('id'), 400);
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
})();
|
|||
|
</script>
|
|||
|
|
|||
|
</div><section class="page__comments d-print-none"></section></article><!-- start custom main bottom snippet -->
|
|||
|
<!-- end custom main bottom snippet -->
|
|||
|
</div>
|
|||
|
</div></div></div></div>
|
|||
|
</div><script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $body = $('body'), $window = $(window);
|
|||
|
var $pageRoot = $('.js-page-root'), $pageMain = $('.js-page-main');
|
|||
|
var activeCount = 0;
|
|||
|
function modal(options) {
|
|||
|
var $root = this, visible, onChange, hideWhenWindowScroll = false;
|
|||
|
var scrollTop;
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
visible = _options.initialVisible === undefined ? false : show;
|
|||
|
onChange = _options.onChange;
|
|||
|
hideWhenWindowScroll = _options.hideWhenWindowScroll;
|
|||
|
}
|
|||
|
function init() {
|
|||
|
setState(visible);
|
|||
|
}
|
|||
|
function setState(isShow) {
|
|||
|
if (isShow === visible) {
|
|||
|
return;
|
|||
|
}
|
|||
|
visible = isShow;
|
|||
|
if (visible) {
|
|||
|
activeCount++;
|
|||
|
scrollTop = $(window).scrollTop() || $pageMain.scrollTop();
|
|||
|
$root.addClass('modal--show');
|
|||
|
$pageMain.scrollTop(scrollTop);
|
|||
|
activeCount === 1 && ($pageRoot.addClass('show-modal'), $body.addClass('of-hidden'));
|
|||
|
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.on('scroll', hide);
|
|||
|
$window.on('keyup', handleKeyup);
|
|||
|
} else {
|
|||
|
activeCount > 0 && activeCount--;
|
|||
|
$root.removeClass('modal--show');
|
|||
|
$window.scrollTop(scrollTop);
|
|||
|
activeCount === 0 && ($pageRoot.removeClass('show-modal'), $body.removeClass('of-hidden'));
|
|||
|
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.off('scroll', hide);
|
|||
|
$window.off('keyup', handleKeyup);
|
|||
|
}
|
|||
|
onChange && onChange(visible);
|
|||
|
}
|
|||
|
function show() {
|
|||
|
setState(true);
|
|||
|
}
|
|||
|
function hide() {
|
|||
|
setState(false);
|
|||
|
}
|
|||
|
function handleKeyup(e) {
|
|||
|
// Char Code: 27 ESC
|
|||
|
if (e.which === 27) {
|
|||
|
hide();
|
|||
|
}
|
|||
|
}
|
|||
|
setOptions(options);
|
|||
|
init();
|
|||
|
return {
|
|||
|
show: show,
|
|||
|
hide: hide,
|
|||
|
$el: $root
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.modal = modal;
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><div class="modal modal--overflow page__search-modal d-print-none js-page-search-modal"><script>
|
|||
|
(function () {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
// search panel
|
|||
|
var search = (window.search || (window.search = {}));
|
|||
|
var useDefaultSearchBox = window.useDefaultSearchBox === undefined ?
|
|||
|
true : window.useDefaultSearchBox ;
|
|||
|
|
|||
|
var $searchModal = $('.js-page-search-modal');
|
|||
|
var $searchToggle = $('.js-search-toggle');
|
|||
|
var searchModal = $searchModal.modal({ onChange: handleModalChange, hideWhenWindowScroll: true });
|
|||
|
var modalVisible = false;
|
|||
|
search.searchModal = searchModal;
|
|||
|
|
|||
|
var $searchBox = null;
|
|||
|
var $searchInput = null;
|
|||
|
var $searchClear = null;
|
|||
|
|
|||
|
function getModalVisible() {
|
|||
|
return modalVisible;
|
|||
|
}
|
|||
|
search.getModalVisible = getModalVisible;
|
|||
|
|
|||
|
function handleModalChange(visible) {
|
|||
|
modalVisible = visible;
|
|||
|
if (visible) {
|
|||
|
search.onShow && search.onShow();
|
|||
|
useDefaultSearchBox && $searchInput[0] && $searchInput[0].focus();
|
|||
|
} else {
|
|||
|
search.onShow && search.onHide();
|
|||
|
useDefaultSearchBox && $searchInput[0] && $searchInput[0].blur();
|
|||
|
setTimeout(function() {
|
|||
|
useDefaultSearchBox && ($searchInput.val(''), $searchBox.removeClass('not-empty'));
|
|||
|
search.clear && search.clear();
|
|||
|
window.pageAsideAffix && window.pageAsideAffix.refresh();
|
|||
|
}, 400);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$searchToggle.on('click', function() {
|
|||
|
modalVisible ? searchModal.hide() : searchModal.show();
|
|||
|
});
|
|||
|
// Char Code: 83 S, 191 /
|
|||
|
$(window).on('keyup', function(e) {
|
|||
|
if (!modalVisible && !window.isFormElement(e.target || e.srcElement) && (e.which === 83 || e.which === 191)) {
|
|||
|
modalVisible || searchModal.show();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if (useDefaultSearchBox) {
|
|||
|
$searchBox = $('.js-search-box');
|
|||
|
$searchInput = $searchBox.children('input');
|
|||
|
$searchClear = $searchBox.children('.js-icon-clear');
|
|||
|
search.getSearchInput = function() {
|
|||
|
return $searchInput.get(0);
|
|||
|
};
|
|||
|
search.getVal = function() {
|
|||
|
return $searchInput.val();
|
|||
|
};
|
|||
|
search.setVal = function(val) {
|
|||
|
$searchInput.val(val);
|
|||
|
};
|
|||
|
|
|||
|
$searchInput.on('focus', function() {
|
|||
|
$(this).addClass('focus');
|
|||
|
});
|
|||
|
$searchInput.on('blur', function() {
|
|||
|
$(this).removeClass('focus');
|
|||
|
});
|
|||
|
$searchInput.on('input', window.throttle(function() {
|
|||
|
var val = $(this).val();
|
|||
|
if (val === '' || typeof val !== 'string') {
|
|||
|
search.clear && search.clear();
|
|||
|
} else {
|
|||
|
$searchBox.addClass('not-empty');
|
|||
|
search.onInputNotEmpty && search.onInputNotEmpty(val);
|
|||
|
}
|
|||
|
}, 400));
|
|||
|
$searchClear.on('click', function() {
|
|||
|
$searchInput.val(''); $searchBox.removeClass('not-empty');
|
|||
|
search.clear && search.clear();
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><div class="search search--dark">
|
|||
|
<div class="main">
|
|||
|
<div class="search__header">Recherche</div>
|
|||
|
<div class="search-bar">
|
|||
|
<div class="search-box js-search-box">
|
|||
|
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
|
|||
|
<input id="search-input" type="text" />
|
|||
|
<div class="search-box__icon-clear js-icon-clear">
|
|||
|
<a><i class="fas fa-times"></i></a>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<button class="button button--theme-dark button--pill search__cancel js-search-toggle">
|
|||
|
Annuler</button>
|
|||
|
</div>
|
|||
|
<div id="results-container" class="search-result js-search-result"></div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
<!-- Script pointing to search-script.js -->
|
|||
|
<script>/*!
|
|||
|
* Simple-Jekyll-Search
|
|||
|
* Copyright 2015-2020, Christian Fei
|
|||
|
* Licensed under the MIT License.
|
|||
|
*/
|
|||
|
|
|||
|
(function(){
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Templater_7 = {
|
|||
|
compile: compile,
|
|||
|
setOptions: setOptions
|
|||
|
}
|
|||
|
|
|||
|
const options = {}
|
|||
|
options.pattern = /\{(.*?)\}/g
|
|||
|
options.template = ''
|
|||
|
options.middleware = function () {}
|
|||
|
|
|||
|
function setOptions (_options) {
|
|||
|
options.pattern = _options.pattern || options.pattern
|
|||
|
options.template = _options.template || options.template
|
|||
|
if (typeof _options.middleware === 'function') {
|
|||
|
options.middleware = _options.middleware
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function compile (data) {
|
|||
|
return options.template.replace(options.pattern, function (match, prop) {
|
|||
|
const value = options.middleware(prop, data[prop], options.template)
|
|||
|
if (typeof value !== 'undefined') {
|
|||
|
return value
|
|||
|
}
|
|||
|
return data[prop] || match
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
'use strict';
|
|||
|
|
|||
|
function fuzzysearch (needle, haystack) {
|
|||
|
var tlen = haystack.length;
|
|||
|
var qlen = needle.length;
|
|||
|
if (qlen > tlen) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (qlen === tlen) {
|
|||
|
return needle === haystack;
|
|||
|
}
|
|||
|
outer: for (var i = 0, j = 0; i < qlen; i++) {
|
|||
|
var nch = needle.charCodeAt(i);
|
|||
|
while (j < tlen) {
|
|||
|
if (haystack.charCodeAt(j++) === nch) {
|
|||
|
continue outer;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
var _$fuzzysearch_1 = fuzzysearch;
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
|
|||
|
|
|||
|
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
|
|||
|
|
|||
|
function FuzzySearchStrategy () {
|
|||
|
this.matches = function (string, crit) {
|
|||
|
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
|
|||
|
|
|||
|
function LiteralSearchStrategy () {
|
|||
|
this.matches = function (str, crit) {
|
|||
|
if (!str) return false
|
|||
|
|
|||
|
str = str.trim().toLowerCase()
|
|||
|
crit = crit.trim().toLowerCase()
|
|||
|
|
|||
|
return crit.split(' ').filter(function (word) {
|
|||
|
return str.indexOf(word) >= 0
|
|||
|
}).length === crit.split(' ').length
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$Repository_4 = {
|
|||
|
put: put,
|
|||
|
clear: clear,
|
|||
|
search: search,
|
|||
|
setOptions: __setOptions_4
|
|||
|
}
|
|||
|
|
|||
|
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
|
|||
|
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
|
|||
|
|
|||
|
function NoSort () {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
const data = []
|
|||
|
let opt = {}
|
|||
|
|
|||
|
opt.fuzzy = false
|
|||
|
opt.limit = 10
|
|||
|
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = NoSort
|
|||
|
opt.exclude = []
|
|||
|
|
|||
|
function put (data) {
|
|||
|
if (isObject(data)) {
|
|||
|
return addObject(data)
|
|||
|
}
|
|||
|
if (isArray(data)) {
|
|||
|
return addArray(data)
|
|||
|
}
|
|||
|
return undefined
|
|||
|
}
|
|||
|
function clear () {
|
|||
|
data.length = 0
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function isObject (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
|
|||
|
}
|
|||
|
|
|||
|
function isArray (obj) {
|
|||
|
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
|
|||
|
}
|
|||
|
|
|||
|
function addObject (_data) {
|
|||
|
data.push(_data)
|
|||
|
return data
|
|||
|
}
|
|||
|
|
|||
|
function addArray (_data) {
|
|||
|
const added = []
|
|||
|
clear()
|
|||
|
for (let i = 0, len = _data.length; i < len; i++) {
|
|||
|
if (isObject(_data[i])) {
|
|||
|
added.push(addObject(_data[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
return added
|
|||
|
}
|
|||
|
|
|||
|
function search (crit) {
|
|||
|
if (!crit) {
|
|||
|
return []
|
|||
|
}
|
|||
|
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
|
|||
|
}
|
|||
|
|
|||
|
function __setOptions_4 (_opt) {
|
|||
|
opt = _opt || {}
|
|||
|
|
|||
|
opt.fuzzy = _opt.fuzzy || false
|
|||
|
opt.limit = _opt.limit || 10
|
|||
|
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
|
|||
|
opt.sort = _opt.sort || NoSort
|
|||
|
opt.exclude = _opt.exclude || []
|
|||
|
}
|
|||
|
|
|||
|
function findMatches (data, crit, strategy, opt) {
|
|||
|
const matches = []
|
|||
|
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
|
|||
|
const match = findMatchesInObject(data[i], crit, strategy, opt)
|
|||
|
if (match) {
|
|||
|
matches.push(match)
|
|||
|
}
|
|||
|
}
|
|||
|
return matches
|
|||
|
}
|
|||
|
|
|||
|
function findMatchesInObject (obj, crit, strategy, opt) {
|
|||
|
for (const key in obj) {
|
|||
|
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
|
|||
|
return obj
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isExcluded (term, excludedTerms) {
|
|||
|
for (let i = 0, len = excludedTerms.length; i < len; i++) {
|
|||
|
const excludedTerm = excludedTerms[i]
|
|||
|
if (new RegExp(excludedTerm).test(term)) {
|
|||
|
return true
|
|||
|
}
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
/* globals ActiveXObject:false */
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$JSONLoader_2 = {
|
|||
|
load: load
|
|||
|
}
|
|||
|
|
|||
|
function load (location, callback) {
|
|||
|
const xhr = getXHR()
|
|||
|
xhr.open('GET', location, true)
|
|||
|
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
|
|||
|
xhr.send()
|
|||
|
}
|
|||
|
|
|||
|
function createStateChangeListener (xhr, callback) {
|
|||
|
return function () {
|
|||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|||
|
try {
|
|||
|
callback(null, JSON.parse(xhr.responseText))
|
|||
|
} catch (err) {
|
|||
|
callback(err, null)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function getXHR () {
|
|||
|
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$OptionsValidator_3 = function OptionsValidator (params) {
|
|||
|
if (!validateParams(params)) {
|
|||
|
throw new Error('-- OptionsValidator: required options missing')
|
|||
|
}
|
|||
|
|
|||
|
if (!(this instanceof OptionsValidator)) {
|
|||
|
return new OptionsValidator(params)
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = params.required
|
|||
|
|
|||
|
this.getRequiredOptions = function () {
|
|||
|
return requiredOptions
|
|||
|
}
|
|||
|
|
|||
|
this.validate = function (parameters) {
|
|||
|
const errors = []
|
|||
|
requiredOptions.forEach(function (requiredOptionName) {
|
|||
|
if (typeof parameters[requiredOptionName] === 'undefined') {
|
|||
|
errors.push(requiredOptionName)
|
|||
|
}
|
|||
|
})
|
|||
|
return errors
|
|||
|
}
|
|||
|
|
|||
|
function validateParams (params) {
|
|||
|
if (!params) {
|
|||
|
return false
|
|||
|
}
|
|||
|
return typeof params.required !== 'undefined' && params.required instanceof Array
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
'use strict'
|
|||
|
|
|||
|
var _$utils_9 = {
|
|||
|
merge: merge,
|
|||
|
isJSON: isJSON
|
|||
|
}
|
|||
|
|
|||
|
function merge (defaultParams, mergeParams) {
|
|||
|
const mergedOptions = {}
|
|||
|
for (const option in defaultParams) {
|
|||
|
mergedOptions[option] = defaultParams[option]
|
|||
|
if (typeof mergeParams[option] !== 'undefined') {
|
|||
|
mergedOptions[option] = mergeParams[option]
|
|||
|
}
|
|||
|
}
|
|||
|
return mergedOptions
|
|||
|
}
|
|||
|
|
|||
|
function isJSON (json) {
|
|||
|
try {
|
|||
|
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
|
|||
|
return true
|
|||
|
}
|
|||
|
return false
|
|||
|
} catch (err) {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var _$src_8 = {};
|
|||
|
(function (window) {
|
|||
|
'use strict'
|
|||
|
|
|||
|
let options = {
|
|||
|
searchInput: null,
|
|||
|
resultsContainer: null,
|
|||
|
json: [],
|
|||
|
success: Function.prototype,
|
|||
|
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
|
|||
|
templateMiddleware: Function.prototype,
|
|||
|
sortMiddleware: function () {
|
|||
|
return 0
|
|||
|
},
|
|||
|
noResultsText: 'No results found',
|
|||
|
limit: 10,
|
|||
|
fuzzy: false,
|
|||
|
debounceTime: null,
|
|||
|
exclude: []
|
|||
|
}
|
|||
|
|
|||
|
let debounceTimerHandle
|
|||
|
const debounce = function (func, delayMillis) {
|
|||
|
if (delayMillis) {
|
|||
|
clearTimeout(debounceTimerHandle)
|
|||
|
debounceTimerHandle = setTimeout(func, delayMillis)
|
|||
|
} else {
|
|||
|
func.call()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
|
|||
|
|
|||
|
/* removed: const _$Templater_7 = require('./Templater') */;
|
|||
|
/* removed: const _$Repository_4 = require('./Repository') */;
|
|||
|
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
|
|||
|
const optionsValidator = _$OptionsValidator_3({
|
|||
|
required: requiredOptions
|
|||
|
})
|
|||
|
/* removed: const _$utils_9 = require('./utils') */;
|
|||
|
|
|||
|
window.SimpleJekyllSearch = function (_options) {
|
|||
|
const errors = optionsValidator.validate(_options)
|
|||
|
if (errors.length > 0) {
|
|||
|
throwError('You must specify the following required options: ' + requiredOptions)
|
|||
|
}
|
|||
|
|
|||
|
options = _$utils_9.merge(options, _options)
|
|||
|
|
|||
|
_$Templater_7.setOptions({
|
|||
|
template: options.searchResultTemplate,
|
|||
|
middleware: options.templateMiddleware
|
|||
|
})
|
|||
|
|
|||
|
_$Repository_4.setOptions({
|
|||
|
fuzzy: options.fuzzy,
|
|||
|
limit: options.limit,
|
|||
|
sort: options.sortMiddleware,
|
|||
|
exclude: options.exclude
|
|||
|
})
|
|||
|
|
|||
|
if (_$utils_9.isJSON(options.json)) {
|
|||
|
initWithJSON(options.json)
|
|||
|
} else {
|
|||
|
initWithURL(options.json)
|
|||
|
}
|
|||
|
|
|||
|
const rv = {
|
|||
|
search: search
|
|||
|
}
|
|||
|
|
|||
|
typeof options.success === 'function' && options.success.call(rv)
|
|||
|
return rv
|
|||
|
}
|
|||
|
|
|||
|
function initWithJSON (json) {
|
|||
|
_$Repository_4.put(json)
|
|||
|
registerInput()
|
|||
|
}
|
|||
|
|
|||
|
function initWithURL (url) {
|
|||
|
_$JSONLoader_2.load(url, function (err, json) {
|
|||
|
if (err) {
|
|||
|
throwError('failed to get JSON (' + url + ')')
|
|||
|
}
|
|||
|
initWithJSON(json)
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function emptyResultsContainer () {
|
|||
|
options.resultsContainer.innerHTML = ''
|
|||
|
}
|
|||
|
|
|||
|
function appendToResultsContainer (text) {
|
|||
|
options.resultsContainer.innerHTML += text
|
|||
|
}
|
|||
|
|
|||
|
function registerInput () {
|
|||
|
options.searchInput.addEventListener('input', function (e) {
|
|||
|
if (isWhitelistedKey(e.which)) {
|
|||
|
emptyResultsContainer()
|
|||
|
debounce(function () { search(e.target.value) }, options.debounceTime)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function search (query) {
|
|||
|
if (isValidQuery(query)) {
|
|||
|
emptyResultsContainer()
|
|||
|
render(_$Repository_4.search(query), query)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function render (results, query) {
|
|||
|
const len = results.length
|
|||
|
if (len === 0) {
|
|||
|
return appendToResultsContainer(options.noResultsText)
|
|||
|
}
|
|||
|
for (let i = 0; i < len; i++) {
|
|||
|
results[i].query = query
|
|||
|
appendToResultsContainer(_$Templater_7.compile(results[i]))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function isValidQuery (query) {
|
|||
|
return query && query.length > 0
|
|||
|
}
|
|||
|
|
|||
|
function isWhitelistedKey (key) {
|
|||
|
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
|
|||
|
}
|
|||
|
|
|||
|
function throwError (message) {
|
|||
|
throw new Error('SimpleJekyllSearch --- ' + message)
|
|||
|
}
|
|||
|
})(window)
|
|||
|
|
|||
|
}());
|
|||
|
</script>
|
|||
|
|
|||
|
<!-- Configuration -->
|
|||
|
<script>
|
|||
|
SimpleJekyllSearch({
|
|||
|
searchInput: document.getElementById('search-input'),
|
|||
|
resultsContainer: document.getElementById('results-container'),
|
|||
|
noResultsText: '<p>Aucun résultat!</p>',
|
|||
|
json: '/search.json',
|
|||
|
searchResultTemplate: '<li><a href="{url}">{date} {title}</a> (Création {create})</li>'
|
|||
|
})
|
|||
|
</script>
|
|||
|
|
|||
|
</div></div>
|
|||
|
|
|||
|
|
|||
|
<script>(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function scrollToAnchor(anchor, duration, callback) {
|
|||
|
var $root = this;
|
|||
|
$root.animate({ scrollTop: $(anchor).position().top }, duration, function() {
|
|||
|
window.history.replaceState(null, '', window.location.href.split('#')[0] + anchor);
|
|||
|
callback && callback();
|
|||
|
});
|
|||
|
}
|
|||
|
$.fn.scrollToAnchor = scrollToAnchor;
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function affix(options) {
|
|||
|
var $root = this, $window = $(window), $scrollTarget, $scroll,
|
|||
|
offsetBottom = 0, scrollTarget = window, scroll = window.document, disabled = false, isOverallScroller = true,
|
|||
|
rootTop, rootLeft, rootHeight, scrollBottom, rootBottomTop,
|
|||
|
hasInit = false, curState;
|
|||
|
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
_options.offsetBottom && (offsetBottom = _options.offsetBottom);
|
|||
|
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
|
|||
|
_options.scroll && (scroll = _options.scroll);
|
|||
|
_options.disabled !== undefined && (disabled = _options.disabled);
|
|||
|
$scrollTarget = $(scrollTarget);
|
|||
|
isOverallScroller = window.isOverallScroller($scrollTarget[0]);
|
|||
|
$scroll = $(scroll);
|
|||
|
}
|
|||
|
function preCalc() {
|
|||
|
top();
|
|||
|
rootHeight = $root.outerHeight();
|
|||
|
rootTop = $root.offset().top + (isOverallScroller ? 0 : $scrollTarget.scrollTop());
|
|||
|
rootLeft = $root.offset().left;
|
|||
|
}
|
|||
|
function calc(needPreCalc) {
|
|||
|
needPreCalc && preCalc();
|
|||
|
scrollBottom = $scroll.outerHeight() - offsetBottom - rootHeight;
|
|||
|
rootBottomTop = scrollBottom - rootTop;
|
|||
|
}
|
|||
|
function top() {
|
|||
|
if (curState !== 'top') {
|
|||
|
$root.removeClass('fixed').css({
|
|||
|
left: 0,
|
|||
|
top: 0
|
|||
|
});
|
|||
|
curState = 'top';
|
|||
|
}
|
|||
|
}
|
|||
|
function fixed() {
|
|||
|
if (curState !== 'fixed') {
|
|||
|
$root.addClass('fixed').css({
|
|||
|
left: rootLeft + 'px',
|
|||
|
top: 0
|
|||
|
});
|
|||
|
curState = 'fixed';
|
|||
|
}
|
|||
|
}
|
|||
|
function bottom() {
|
|||
|
if (curState !== 'bottom') {
|
|||
|
$root.removeClass('fixed').css({
|
|||
|
left: 0,
|
|||
|
top: rootBottomTop + 'px'
|
|||
|
});
|
|||
|
curState = 'bottom';
|
|||
|
}
|
|||
|
}
|
|||
|
function setState() {
|
|||
|
var scrollTop = $scrollTarget.scrollTop();
|
|||
|
if (scrollTop >= rootTop && scrollTop <= scrollBottom) {
|
|||
|
fixed();
|
|||
|
} else if (scrollTop < rootTop) {
|
|||
|
top();
|
|||
|
} else {
|
|||
|
bottom();
|
|||
|
}
|
|||
|
}
|
|||
|
function init() {
|
|||
|
if(!hasInit) {
|
|||
|
var interval, timeout;
|
|||
|
calc(true); setState();
|
|||
|
// run calc every 100 millisecond
|
|||
|
interval = setInterval(function() {
|
|||
|
calc();
|
|||
|
}, 100);
|
|||
|
timeout = setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
}, 45000);
|
|||
|
window.pageLoad.then(function() {
|
|||
|
setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
clearTimeout(timeout);
|
|||
|
}, 3000);
|
|||
|
});
|
|||
|
$scrollTarget.on('scroll', function() {
|
|||
|
disabled || setState();
|
|||
|
});
|
|||
|
$window.on('resize', function() {
|
|||
|
disabled || (calc(true), setState());
|
|||
|
});
|
|||
|
hasInit = true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
setOptions(options);
|
|||
|
if (!disabled) {
|
|||
|
init();
|
|||
|
}
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
init();
|
|||
|
}, 200));
|
|||
|
return {
|
|||
|
setOptions: setOptions,
|
|||
|
refresh: function() {
|
|||
|
calc(true, { animation: false }); setState();
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.affix = affix;
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
function toc(options) {
|
|||
|
var $root = this, $window = $(window), $scrollTarget, $scroller, $tocUl = $('<ul class="toc toc--ellipsis"></ul>'), $tocLi, $headings, $activeLast, $activeCur,
|
|||
|
selectors = 'h1,h2,h3', container = 'body', scrollTarget = window, scroller = 'html, body', disabled = false,
|
|||
|
headingsPos, scrolling = false, hasRendered = false, hasInit = false;
|
|||
|
|
|||
|
function setOptions(options) {
|
|||
|
var _options = options || {};
|
|||
|
_options.selectors && (selectors = _options.selectors);
|
|||
|
_options.container && (container = _options.container);
|
|||
|
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
|
|||
|
_options.scroller && (scroller = _options.scroller);
|
|||
|
_options.disabled !== undefined && (disabled = _options.disabled);
|
|||
|
$headings = $(container).find(selectors).filter('[id]');
|
|||
|
$scrollTarget = $(scrollTarget);
|
|||
|
$scroller = $(scroller);
|
|||
|
}
|
|||
|
function calc() {
|
|||
|
headingsPos = [];
|
|||
|
$headings.each(function() {
|
|||
|
headingsPos.push(Math.floor($(this).position().top));
|
|||
|
});
|
|||
|
}
|
|||
|
function setState(element, disabled) {
|
|||
|
var scrollTop = $scrollTarget.scrollTop(), i;
|
|||
|
if (disabled || !headingsPos || headingsPos.length < 1) { return; }
|
|||
|
if (element) {
|
|||
|
$activeCur = element;
|
|||
|
} else {
|
|||
|
for (i = 0; i < headingsPos.length; i++) {
|
|||
|
if (scrollTop >= headingsPos[i]) {
|
|||
|
$activeCur = $tocLi.eq(i);
|
|||
|
} else {
|
|||
|
$activeCur || ($activeCur = $tocLi.eq(i));
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
$activeLast && $activeLast.removeClass('active');
|
|||
|
($activeLast = $activeCur).addClass('active');
|
|||
|
}
|
|||
|
function render() {
|
|||
|
if(!hasRendered) {
|
|||
|
$root.append($tocUl);
|
|||
|
$headings.each(function() {
|
|||
|
var $this = $(this);
|
|||
|
$tocUl.append($('<li></li>').addClass('toc-' + $this.prop('tagName').toLowerCase())
|
|||
|
.append($('<a></a>').text($this.text()).attr('href', '#' + $this.prop('id'))));
|
|||
|
});
|
|||
|
$tocLi = $tocUl.children('li');
|
|||
|
$tocUl.on('click', 'a', function(e) {
|
|||
|
e.preventDefault();
|
|||
|
var $this = $(this);
|
|||
|
scrolling = true;
|
|||
|
setState($this.parent());
|
|||
|
$scroller.scrollToAnchor($this.attr('href'), 400, function() {
|
|||
|
scrolling = false;
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
hasRendered = true;
|
|||
|
}
|
|||
|
function init() {
|
|||
|
var interval, timeout;
|
|||
|
if(!hasInit) {
|
|||
|
render(); calc(); setState(null, scrolling);
|
|||
|
// run calc every 100 millisecond
|
|||
|
interval = setInterval(function() {
|
|||
|
calc();
|
|||
|
}, 100);
|
|||
|
timeout = setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
}, 45000);
|
|||
|
window.pageLoad.then(function() {
|
|||
|
setTimeout(function() {
|
|||
|
clearInterval(interval);
|
|||
|
clearTimeout(timeout);
|
|||
|
}, 3000);
|
|||
|
});
|
|||
|
$scrollTarget.on('scroll', function() {
|
|||
|
disabled || setState(null, scrolling);
|
|||
|
});
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
if (!disabled) {
|
|||
|
render(); calc(); setState(null, scrolling);
|
|||
|
}
|
|||
|
}, 100));
|
|||
|
}
|
|||
|
hasInit = true;
|
|||
|
}
|
|||
|
|
|||
|
setOptions(options);
|
|||
|
if (!disabled) {
|
|||
|
init();
|
|||
|
}
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
init();
|
|||
|
}, 200));
|
|||
|
return {
|
|||
|
setOptions: setOptions
|
|||
|
};
|
|||
|
}
|
|||
|
$.fn.toc = toc;
|
|||
|
});
|
|||
|
})();
|
|||
|
/*(function () {
|
|||
|
|
|||
|
})();*/
|
|||
|
</script><script>
|
|||
|
/* toc must before affix, since affix need to konw toc' height. */(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
var TOC_SELECTOR = window.TEXT_VARIABLES.site.toc.selectors;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $window = $(window);
|
|||
|
var $articleContent = $('.js-article-content');
|
|||
|
var $tocRoot = $('.js-toc-root'), $col2 = $('.js-col-aside');
|
|||
|
var toc;
|
|||
|
var tocDisabled = false;
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
var hasToc = $articleContent.find(TOC_SELECTOR).length > 0;
|
|||
|
|
|||
|
function disabled() {
|
|||
|
return $col2.css('display') === 'none' || !hasToc;
|
|||
|
}
|
|||
|
|
|||
|
tocDisabled = disabled();
|
|||
|
|
|||
|
toc = $tocRoot.toc({
|
|||
|
selectors: TOC_SELECTOR,
|
|||
|
container: $articleContent,
|
|||
|
scrollTarget: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroller: hasSidebar ? '.js-page-main' : null,
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
tocDisabled = disabled();
|
|||
|
toc && toc.setOptions({
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
}, 100));
|
|||
|
|
|||
|
});
|
|||
|
})();
|
|||
|
(function() {
|
|||
|
var SOURCES = window.TEXT_VARIABLES.sources;
|
|||
|
window.Lazyload.js(SOURCES.jquery, function() {
|
|||
|
var $window = $(window), $pageFooter = $('.js-page-footer');
|
|||
|
var $pageAside = $('.js-page-aside');
|
|||
|
var affix;
|
|||
|
var tocDisabled = false;
|
|||
|
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
|
|||
|
|
|||
|
affix = $pageAside.affix({
|
|||
|
offsetBottom: $pageFooter.outerHeight(),
|
|||
|
scrollTarget: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroller: hasSidebar ? '.js-page-main' : null,
|
|||
|
scroll: hasSidebar ? $('.js-page-main').children() : null,
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
|
|||
|
$window.on('resize', window.throttle(function() {
|
|||
|
affix && affix.setOptions({
|
|||
|
disabled: tocDisabled
|
|||
|
});
|
|||
|
}, 100));
|
|||
|
|
|||
|
window.pageAsideAffix = affix;
|
|||
|
});
|
|||
|
})();
|
|||
|
</script><!---->
|
|||
|
|
|||
|
</div>
|
|||
|
<script>(function () {
|
|||
|
var $root = document.getElementsByClassName('root')[0];
|
|||
|
if (window.hasEvent('touchstart')) {
|
|||
|
$root.dataset.isTouch = true;
|
|||
|
document.addEventListener('touchstart', function(){}, false);
|
|||
|
}
|
|||
|
})();
|
|||
|
</script>
|
|||
|
</body>
|
|||
|
</html>
|
|||
|
|