bugfix (atomic update): update to indexDB weren't atomic causing out

of sync issues
This commit is contained in:
Mickael KERJEAN
2018-04-16 19:22:51 +10:00
parent 5a913d44d1
commit 7e62478f6f
7 changed files with 330 additions and 432 deletions
+65 -87
View File
@@ -1,14 +1,9 @@
"use strict";
// window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
// window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction || {READ_WRITE: "readwrite"}; // This line should only be needed if it is needed to support the object's constants for older browsers
// window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
function Data(){
this.FILE_PATH = "file_path";
this.FILE_CONTENT = "file_content";
this.db_version = 'v1.0';
this.db = null;
this.intervalId = window.setInterval(this._vacuum.bind(this), 5000);
this._init();
}
@@ -37,14 +32,10 @@ Data.prototype._setup = function(db){
}
}
Data.prototype._vacuum = function(){
}
/*
* Fetch a record using its path, can be whether a file path or content
*/
Data.prototype.get = function(type, path, _record_access = true){
Data.prototype.get = function(type, path){
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
return this.db.then((db) => {
@@ -55,47 +46,68 @@ Data.prototype.get = function(type, path, _record_access = true){
query.onsuccess = (e) => {
let data = query.result;
done(query.result || null);
if(data && _record_access === true){
data.last_access = new Date();
if(!data.access_count) data.access_count = 0;
data.access_count += 1;
window.requestAnimationFrame(() => {
this.put(type, data.path, data);
});
}
};
tx.onerror = error;
query.onerror = error;
});
});
}
Data.prototype.put = function(type, path, data){
Data.prototype.update = function(type, path, fn, exact = true){
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
const range = exact === true? IDBKeyRange.only(path) : IDBKeyRange.bound(
path,
path+'\uFFFF',
false, true
);
const request = store.openCursor(range);
let new_data = null;
return new Promise((done, err) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(!cursor) return done(new_data);
new_data = fn(cursor.value || null);
cursor.delete(cursor.value.path);
store.put(new_data);
cursor.continue();
};
});
});
}
Data.prototype.upsert = function(type, path, fn){
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
const query = store.get(path);
return new Promise((done, error) => {
query.onsuccess = (e) => {
const new_data = fn(query.result || null);
if(!new_data) return done(query.result);
const request = store.put(new_data);
request.onsuccess = () => done(new_data);
request.onerror = (e) => error(e);
};
query.onerror = error;
});
});
}
Data.prototype.add = function(type, path, data){
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
return this.get(type, path, false)
.then((res) => {
let new_data;
if(res === null){
new_data = data;
new_data.last_update = new Date();
new_data.path = path;
}else{
new_data = Object.assign(res, data);
}
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
return new Promise((done, error) => {
let request = store.put(new_data);
request.onsuccess = () => done(new_data.result || new_data.results);
request.onerror = (e) => error(e);
tx.onerror = (e) => error(e);
tx.oncomplete = () => done(new_data.result || new_data.results);
});
});
return this.db.then((db) => {
return new Promise((done, error) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
const request = store.put(data);
request.onsuccess = () => done(data);
request.onerror = (e) => error(e);
});
});
}
Data.prototype.remove = function(type, path, exact = true){
@@ -110,14 +122,16 @@ Data.prototype.remove = function(type, path, exact = true){
req.onerror = err;
});
}else{
const request = store.openCursor();
const request = store.openCursor(IDBKeyRange.bound(
path,
path+'\uFFFF',
true, true
));
return new Promise((done, err) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(cursor){
if(cursor.value.path.indexOf(path) === 0 && path !== cursor.value.path){
store.delete(cursor.value.path);
}
cursor.delete(cursor.value.path);
cursor.continue();
}else{
done();
@@ -128,37 +142,22 @@ Data.prototype.remove = function(type, path, exact = true){
});
}
Data.prototype.update_path = function(updater_fn){
Data.prototype.fetchAll = function(fn, type = this.FILE_PATH){
return this.db.then((db) => {
const tx = db.transaction(this.FILE_PATH, "readwrite");
const store = tx.objectStore(this.FILE_PATH);
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
const request = store.openCursor();
return new Promise((done, error) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(!cursor) return done();
updater_fn(cursor.value, store)
const new_value = fn(cursor.value);
cursor.continue();
};
});
});
}
Data.prototype.update_content = function(updater_fn){
this.db.then((db) => {
const tx = db.transaction(this.FILE_CONTENT, "readwrite");
const store = tx.objectStore(this.FILE_CONTENT);
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if(cursor){
const action = updater_fn(cursor.value, store);
cursor.continue();
}
};
});
}
Data.prototype.destroy = function(){
this.db.then((db) => db.close())
@@ -167,27 +166,6 @@ Data.prototype.destroy = function(){
this._init();
}
// // test
// cache = new Data();
// cache.put(cache.FILE_PATH, '/', {a:3});
// cache.get(cache.FILE_PATH, '/').then((r) => {
// console.log(r);
// cache.remove(cache.FILE_PATH, '/');
// cache.get(cache.FILE_PATH, '/').then((r) => {
// console.log(r);
// //cache.destroy();
// });
// });
export const cache = new Data();
window._cache = cache;
_cache._debug = () => {
window._cache.update_path((e) => window.log("- path: "+e.path, e.results.slice(0,5).map((f) => {
if(f.type === 'directory'){ f.name = "d:"+f.name;} else{ f.name = "f:"+f.name;}
if(f.icon === 'loading') return "("+f.name+")";
else if(f.icon === 'error') return "_"+f.name+"_";
return f.name;
})));
}
+226 -252
View File
@@ -1,6 +1,6 @@
"use strict";
import { http_get, http_post, prepare, basename, dirname } from '../helpers/';
import { http_get, http_post, prepare, basename, dirname, pathBuilder } from '../helpers/';
import Path from 'path';
import { Observable } from 'rxjs/Observable';
@@ -29,7 +29,8 @@ class FileSystem{
.then(() => {
if(keep_pulling_from_http === false) return Promise.resolve();
return fetch_from_http(_path);
});
})
.catch(() => {});
};
fetch_from_http(path);
});
@@ -43,16 +44,25 @@ class FileSystem{
_ls_from_http(path){
const url = '/api/files/ls?path='+prepare(path);
return http_get(url).then((response) => {
return cache.get(cache.FILE_PATH, path, false).then((_files) => {
return cache.upsert(cache.FILE_PATH, path, (_files) => {
let store = Object.assign({
path: path,
results: null,
access_count: 0,
}, _files);
store.access_count += 1;
store.results = response.results;
if(_files && _files.results){
// find out which entry we want to keep from the cache
let _files_virtual_to_keep = _files.results.filter((file) => {
return file.icon === 'loading';
});
// update file results
// update file results when something is going on
for(let i=0; i<_files_virtual_to_keep.length; i++){
for(let j=0; j<response.results.length; j++){
if(response.results[j].name === _files_virtual_to_keep[i].name){
response.results[j] = Object.assign({}, _files_virtual_to_keep[i]);
for(let j=0; j<store.results.length; j++){
if(store.results[j].name === _files_virtual_to_keep[i].name){
store.results[j] = Object.assign({}, _files_virtual_to_keep[i]);
_files_virtual_to_keep[i] = null;
break;
}
@@ -60,44 +70,59 @@ class FileSystem{
}
// add stuff that didn't exist in our response
_files_virtual_to_keep = _files_virtual_to_keep.filter((e) => e);
response.results = response.results.concat(_files_virtual_to_keep);
store.results = store.results.concat(_files_virtual_to_keep);
}
// publish
cache.put(cache.FILE_PATH, path, {results: response.results});
if(this.current_path === path) this.obs && this.obs.next({status: 'ok', results: response.results});
if(this.current_path === path){
this.obs && this.obs.next({status: 'ok', results: store.results});
}
store.last_update = new Date();
store.last_access = new Date();
return store;
});
}).catch((_err) => {
this.obs.next(_err);
return Promise.reject();
});
}
_ls_from_cache(path, _record_access = false){
return cache.get(cache.FILE_PATH, path, _record_access).then((_files) => {
if(_files && _files.results){
if(_record_access === false){
return cache.get(cache.FILE_PATH, path).then((response) => {
if(!response || !response.results) return null;
if(this.current_path === path){
this.obs && this.obs.next({status: 'ok', results: _files.results});
this.obs && this.obs.next({status: 'ok', results: response.results});
}
};
return Promise.resolve();
});
return Promise.resolve();
});
}else{
return cache.upsert(cache.FILE_PATH, path, (response) => {
if(!response || !response.results) return null;
if(this.current_path === path){
this.obs && this.obs.next({status: 'ok', results: response.results});
}
response.last_access = new Date();
response.access_count += 1;
return response;
});
}
}
rm(path){
const url = '/api/files/rm?path='+prepare(path);
return this._replace(path, 'loading')
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
.then(() => http_get(url))
.then((res) => {
if(res.status === 'ok'){
cache.remove(cache.FILE_CONTENT, path, false);
cache.remove(cache.FILE_PATH, dirname(path), false);
return this._remove(path);
}else{
return this._replace(path, 'error');
}
return cache.remove(cache.FILE_CONTENT, path)
.then(cache.remove(cache.FILE_CONTENT, path, false))
.then(cache.remove(cache.FILE_PATH, dirname(path), false))
.then(this._remove(path, 'loading'))
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
})
.catch((err) => {
this._replace(path, 'error');
return Promise.reject(err);
return this._replace(path, 'error', 'loading')
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
.then(() => Promise.reject(err));
});
}
@@ -105,26 +130,36 @@ class FileSystem{
const url = '/api/files/cat?path='+prepare(path);
return http_get(url, 'raw')
.then((res) => {
if(is_binary(res) === false) cache.put(cache.FILE_CONTENT, path, {result: res});
return Promise.resolve(res);
if(this.is_binary(res) === true){
return Promise.reject({code: 'BINARY_FILE'});
}
return cache.upsert(cache.FILE_CONTENT, path, (response) => {
let file = response? response : {
path: path,
last_update: null,
last_access: null,
access_count: -1,
result: null
};
file.result = res;
file.access_count += 1;
file.last_access = new Date();
return file;
}).then((response) => Promise.resolve(response.result));
})
.catch((res) => {
return cache.get(cache.FILE_CONTENT, path)
.then((_res) => {
if(!_res || !_res.result) return Promise.reject(res);
return Promise.resolve(_res.result);
})
.catch(() => Promise.reject(res));
})
.then((res) => {
if(is_binary(res) === true) return Promise.reject({code: 'BINARY_FILE'});
return Promise.resolve(res);
});
.catch((err) => {
console.log("ERROR");
if(err.code === 'BINARY_FILE') return Promise.reject(err);
function is_binary(str){
// Reference: https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character
return /\ufffd/.test(str);
}
return cache.update(cache.FILE_CONTENT, path, (response) => {
response.last_access = new Date();
response.access_count += 1;
return response;
}).then((response) => {
if(!response || !response.result) return Promise.reject(err);
return Promise.resolve(response.result);
});
});
}
url(path){
const url = '/api/files/cat?path='+prepare(path);
@@ -136,29 +171,55 @@ class FileSystem{
let formData = new window.FormData();
formData.append('file', file);
return this._replace(path, 'loading')
.then(() => cache.put(cache.FILE_CONTENT, path, file))
.then(() => http_post(url, formData, 'multipart'))
.then((res)=> res.status === 'ok'? this._replace(path) : this._replace(path, 'error'))
.then(() => {
return this._saveFileToCache(path, file)
.then(() => this._replace(path, null, 'loading'))
.then(() => this._refresh(path))
})
.catch((err) => {
this._replace(path, 'error');
return Promise.reject(err);
return this._replace(path, 'error', 'loading')
.then(() => this._refresh(path))
.then(() => Promise.reject(err))
});
}
mkdir(path){
const url = '/api/files/mkdir?path='+prepare(path);
return this._add(path, 'loading')
.then(() => this._add(path, 'loading'))
const url = '/api/files/mkdir?path='+prepare(path),
origin_path = pathBuilder(this.current_path, basename(path), 'directoy'),
destination_path = path;
return this._add(destination_path, 'loading')
.then(() => origin_path !== destination_path ? this._add(origin_path, 'loading') : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path))
.then(() => http_get(url))
.then((res) => res.status === 'ok'? this._replace(path) : this._replace(path, 'error'))
.then(() => {
return this._replace(destination_path, null, 'loading')
.then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve())
.then(() => cache.add(cache.FILE_PATH, destination_path, {
path: destination_path,
results: [],
access_count: 0,
last_access: null,
last_update: new Date()
}))
.then(() => this._refresh(origin_path, destination_path));
})
.catch((err) => {
this._replace(path, 'error');
this._replace(origin_path, 'error', 'loading')
.then(() => origin_path !== destination_path ? this._remove(destination_path, 'loading') : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path));
return Promise.reject(err);
});
}
touch(path, file){
return this._add(path, 'loading')
const origin_path = pathBuilder(this.current_path, basename(path), 'file'),
destination_path = path;
return this._add(destination_path, 'loading')
.then(() => origin_path !== destination_path ? this._add(origin_path, 'loading') : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path))
.then(() => {
if(file){
const url = '/api/files/cat?path='+prepare(path);
@@ -170,176 +231,56 @@ class FileSystem{
return http_get(url);
}
})
.then((res) => res.status === 'ok'? this._replace(path) : this._replace(path, 'error'))
.then(() => {
this._saveFileToCache(path, file)
.then(() => this._replace(destination_path, null, 'loading'))
.then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path))
})
.catch((err) => {
this._replace(path, 'error');
this._replace(origin_path, 'error', 'loading')
.then(() => origin_path !== destination_path ? this._remove(destination_path, 'loading') : Promise.resolve())
.then(() => this._refresh(origin_path, destination_path));
return Promise.reject(err);
});
}
mv(from, to){
const url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to);
const url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to),
origin_path = from,
destination_path = to;
return ui_before_request(from, to)
.then(() => this._ls_from_cache(dirname(from)))
.then(() => this._ls_from_cache(dirname(to)))
.then(() => http_get(url).then((res) => {
if(res.status === 'ok'){
return ui_when_success.call(this, from, to)
.then(() => this._ls_from_cache(dirname(from)))
.then(() => this._ls_from_cache(dirname(to)));
}else{
return ui_when_fail.call(this, from, to)
.then(() => this._ls_from_cache(dirname(from)))
.then(() => this._ls_from_cache(dirname(to)));
}
}))
.catch((err) => {
ui_when_fail.call(this, from, to)
.then(() => this._ls_from_cache(dirname(from)))
.then(() => this._ls_from_cache(dirname(to)));
return Promise.reject(err);
});
function ui_before_request(from, to){
return update_from()
.then((file) => {
if(dirname(from) !== dirname(to)){
return update_to(file);
}
return Promise.resolve();
});
function update_from(){
return cache.get(cache.FILE_PATH, dirname(from), false)
.then((res_from) => {
let _file = {
name: basename(from),
type: /\/$/.test(from) ? 'directory' : 'file'
};
res_from.results = res_from.results.map((file) => {
if(file.name === basename(from)){
file.name = basename(to);
file.icon = 'loading';
_file = file;
}
return file;
});
return cache.put(cache.FILE_PATH, dirname(from), res_from)
.then(() => Promise.resolve(_file));
});
}
function update_to(file){
return cache.get(cache.FILE_PATH, dirname(to), false).then((res_to) => {
if(!res_to || !res_to.results) return Promise.resolve();
res_to.results.push(file);
return cache.put(cache.FILE_PATH, dirname(to), res_to);
});
}
}
function ui_when_fail(from, to){
return update_from()
.then((file) => {
if(dirname(from) !== dirname(to)){
return update_to();
}
return Promise.resolve();
});
function update_from(){
return cache.get(cache.FILE_PATH, dirname(from), false)
.then((res_from) => {
if(!res_from || !res_from.results) return Promise.reject();
res_from.results = res_from.results.map((file) => {
if(
(dirname(from) === dirname(to) && file.name === basename(to)) ||
(dirname(from) !== dirname(to) && file.name === basename(from))
){
file.icon = 'error';
}
return file;
});
return cache.put(cache.FILE_PATH, dirname(from), res_from)
.then(() => Promise.resolve());
});
}
function update_to(){
return cache.get(cache.FILE_PATH, dirname(to), false)
.then((res_to) => {
if(!res_to || !res_to.results) return Promise.resolve();
res_to.results = res_to.results.filter((file) => {
if(file.name === basename(to)){
return false;
}
return true;
});
return cache.put(cache.FILE_PATH, dirname(from), res_to);
});
}
}
function ui_when_success(from, to){
if(dirname(from) === dirname(to)){
return this._replace(dirname(from)+basename(to), null);
}else{
return update_from()
.then(update_to)
.then(update_related);
}
function update_from(){
return cache.get(cache.FILE_PATH, dirname(from), false).then((res_from) => {
if(!res_from || !res_from.results) return Promise.resolve();
res_from.results = res_from.results.filter((file) => {
if(file.name === basename(to)){
return false;
}
return true;
});
return cache.put(cache.FILE_PATH, dirname(from), res_from);
});
}
function update_to(){
return cache.get(cache.FILE_PATH, dirname(to), false).then((res_to) => {
const target_already_exist = res_to && res_to.results ? true : false;
if(target_already_exist){
res_to.results = res_to.results.map((file) => {
if(file.name === basename(to)){
delete file.icon;
}
return file;
});
return cache.put(cache.FILE_PATH, dirname(to), res_to);
}else{
const data = {results: [{
name: basename(to),
type: /\/$/.test(to) ? 'directory' : 'file',
time: (new Date()).getTime()
}]};
return cache.put(cache.FILE_PATH, dirname(to), data);
}
});
}
function update_related(){
// manage nested directories when we try to rename a directory
if(/\/$/.test(from) === true){
return cache.update_path((data) => {
if(data.path !== dirname(to) && data.path !== dirname(from) && data.path.indexOf(dirname(from)) === 0){
const old_path = data.path;
data.path = data.path.replace(dirname(from), dirname(to));
return cache.remove(cache.FILE_PATH, old_path)
.then(() => cache.put(cache.FILE_PATH, data.path, data));
}
return this._replace(origin_path, 'loading')
.then(this._add(destination_path, 'loading'))
.then(() => this._refresh(origin_path, destination_path))
.then(() => http_get(url))
.then((res) => {
return this._remove(origin_path, 'loading')
.then(() => this._replace(destination_path, null, 'loading'))
.then(() => this._refresh(origin_path, destination_path))
.then(() => {
cache.update(cache.FILE_PATH, origin_path, (data) => {
data.path = data.path.replace(origin_path, destination_path);
return data;
}, false);
cache.update(cache.FILE_CONTENT, origin_path, (data) => {
data.path = data.path.replace(origin_path, destination_path);
return data;
}, false);
return Promise.resolve();
});
}
}
}
})
.catch((err) => {
this._replace(origin_path, 'error', 'loading')
.then(() => this._remove(destination_path, 'loading'))
.then(() => this._refresh(origin_path, destination_path))
return Promise.reject(err);
});
}
frequents(){
let data = [];
return cache.update_path((value) => {
return cache.fetchAll((value) => {
if(value.access_count >= 1 && value.path !== "/"){
data.push(value);
}
@@ -353,52 +294,85 @@ class FileSystem{
});
}
_replace(path, icon){
return cache.get(cache.FILE_PATH, dirname(path), false)
.then((res) => {
if(!res) return Promise.resolve();
let files = res.results.map((file) => {
if(file.name === basename(path)){
if(!icon){
delete file.icon;
}
if(icon){
file.icon = icon;
}
}
return file;
});
res.results = files;
return cache.put(cache.FILE_PATH, dirname(path), res)
.then((res) => this._ls_from_cache(dirname(path)));
_saveFileToCache(path, file){
if(!file) return update_cache("");
return new Promise((done, err) => {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = () => this.is_binary(reader.result) === false? update_cache(reader.result).then(done) : done();
reader.onerror = (err) => err(err);
});
function update_cache(result){
return cache.upsert(cache.FILE_CONTENT, path, (response) => {
if(!response) response = {
path: path,
last_access: null,
last_update: null,
result: null,
access_count: 0
};
response.last_update = new Date();
response.result = result;
return response;
});
}
}
_add(path, icon){
return cache.get(cache.FILE_PATH, dirname(path), false)
.then((res) => {
if(!res) return Promise.resolve();
let file = {
name: basename(path),
type: /\/$/.test(path) ? 'directory' : 'file'
};
if(icon) file.icon = icon;
res.results.push(file);
return cache.put(cache.FILE_PATH, dirname(path), res)
.then((res) => this._ls_from_cache(dirname(path)));
});
_refresh(origin_path, destination_path){
if(this.current_path === dirname(origin_path) ||
this.current_path === dirname(destination_path)){
return this._ls_from_cache(this.current_path);
}
return Promise.resolve();
}
_remove(path){
return cache.get(cache.FILE_PATH, dirname(path), false)
.then((res) => {
if(!res) return Promise.resolve();
let files = res.results.filter((file) => {
return file.name === basename(path) ? false : true;
});
res.results = files;
return cache.put(cache.FILE_PATH, dirname(path), res)
.then((res) => this._ls_from_cache(dirname(path)));
_replace(path, icon, icon_previous){
return cache.update(cache.FILE_PATH, dirname(path), function(res){
res.results = res.results.map((file) => {
if(file.name === basename(path) && file.icon == icon_previous){
if(!icon){ delete file.icon; }
if(icon){ file.icon = icon; }
}
return file;
});
return res;
});
}
_add(path, icon){
return cache.upsert(cache.FILE_PATH, dirname(path), function(res){
if(!res || !res.results){
res = {
path: dirname(path),
results: [],
access_count: 0,
last_access: null,
last_update: new Date()
};
}
let file = {
name: basename(path),
type: /\/$/.test(path) ? 'directory' : 'file'
};
if(icon) file.icon = icon;
res.results.push(file);
return res;
});
}
_remove(path, previous_icon){
return cache.update(cache.FILE_PATH, dirname(path), function(res){
if(!res) return null;
res.results = res.results.filter((file) => {
return file.name === basename(path) && file.icon == previous_icon ? false : true;
});
return res;
});
}
is_binary(str){
// Reference: https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character
return /\ufffd/.test(str);
}
}
+27 -84
View File
@@ -23,6 +23,7 @@ export class FilesPage extends React.Component {
error: false,
height: null
};
this.resetHeight = debounce(this.resetHeight.bind(this), 100);
this.goToFiles = goToFiles.bind(null, this.props.history);
this.goToViewer = goToViewer.bind(null, this.props.history);
@@ -105,11 +106,18 @@ export class FilesPage extends React.Component {
}
}
onCreate(path, type, file){
onCreate(path, type, file, id){
console.log("> creating ("+id+")\t:"+Path.basename(path));
if(type === 'file'){
return Files.touch(path, file)
.then(() => notify.send('A file named "'+Path.basename(path)+'" was created', 'success'))
.catch((err) => notify.send(err, 'error'));
.then(() => {
notify.send('A file named "'+Path.basename(path)+'" was created', 'success')
return Promise.resolve();
})
.catch((err) => {
notify.send(err, 'error')
return Promise.reject(err);
});
}else if(type === 'directory'){
return Files.mkdir(path)
.then(() => notify.send('A folder named "'+Path.basename(path)+'" was created', 'success'))
@@ -130,90 +138,25 @@ export class FilesPage extends React.Component {
}
onUpload(path, files){
const createFilesInUI = (_files) => {
const newfiles = _files.map((file) => {
return {
time: new Date().getTime(),
name: file.name,
type: 'file',
size: file.size,
icon: 'loading'
};
});
const files = JSON.parse(JSON.stringify(this.state.files));
this.setState({files: [].concat(newfiles, files)});
return Promise.resolve(_files);
};
const MAX_POOL_SIZE = 2;
const processes = files.map((file) => {
return this.onCreate.bind(this, Path.join(path, file.name), 'file', file);
});
const processFile = (file) => {
return this.onCreate(Path.join(path, file.name), 'file', file);
};
console.log("- starting: "+processes.length);
Promise.all(Array.apply(null, Array(MAX_POOL_SIZE)).map((e,index) => {
return runner(index);
})).then(() => {
console.log("DONE: "+processes.length);
}).catch((err) => {
console.log("ERROR"+processes.length, err);
});
const updateUI = (filename) => {
const files = JSON.parse(JSON.stringify(this.state.files))
.map((file) => {
// persist file in UI
if(file.name === filename){
file.virtual = false;
delete file.icon;
}
// remove from ui if we upload the file in a different directory
return path === this.state.path ? file : null;
})
.filter((file) => {
return file === null? false : true;
});
this.setState({files: files});
return Promise.resolve('ok');
};
const showError = (filename, err) => {
if(err && err.code === 'CANCELLED'){ return }
const files = JSON.parse(JSON.stringify(this.state.files))
.map((file) => {
if(file.name === filename){
file.icon = 'error';
file.message = err && err.message || 'oups something went wrong';
file.virtual = true;
}
return file;
});
this.setState({files: files});
return Promise.resolve('ok');
};
function generator(arr){
let store = arr;
return {
next: function(){
return store.pop();
}
};
function runner(id){
if(processes.length === 0) return Promise.resolve();
return processes.shift()(id)
.then(() => runner(id));
}
function job(it){
let file = it.next();
if(file){
return processFile(file)
.then((ok) => updateUI(file.name))
.then(() => job(it))
.catch((err) => showError(file.name, err));
}else{
return Promise.resolve('ok');
}
}
function process(it, pool){
return Promise.all(Array.apply(null, Array(pool)).map(() => {
return job(it);
}));
}
const poolSize = 10;
return createFilesInUI(files)
.then((files) => Promise.resolve(generator(files)))
.then((it) => process(it, poolSize))
.then((res) => Promise.resolve('ok'));
}
resetHeight(){
+1 -1
View File
@@ -102,7 +102,7 @@ export class FileSystem extends React.Component {
{
this.sort(this.props.files, this.state.sort).map((file, index) => {
if(file.type === 'directory' || file.type === 'file' || file.type === 'link' || file.type === 'bucket'){
return <ExistingThing key={file.name} file={file} path={this.props.path} />
return <ExistingThing key={file.name+(file.icon || '')} file={file} path={this.props.path} />
}
})
}
+3 -3
View File
@@ -88,8 +88,8 @@ export class ExistingThing extends React.Component {
onRename(newFilename){
this.props.emit(
'file.rename',
pathBuilder(this.props.path, this.props.file.name),
pathBuilder(this.props.path, newFilename),
pathBuilder(this.props.path, this.props.file.name, this.props.file.type),
pathBuilder(this.props.path, newFilename, this.props.file.type),
this.props.file.type
);
this.setState({is_renaming: false});
@@ -156,7 +156,7 @@ export class ExistingThing extends React.Component {
return connectDragSource(connectDropNativeFile(connectDropFile(
<div className="component_thing">
<Link to={this.props.file.link}>
<Link to={this.props.file.link || "#"}>
<Card className={this.state.hover} className={className}>
<Icon name={this.props.file.icon || this.props.file.type} />
<Filename filename={this.props.file.name} filesize={this.props.file.size} filetype={this.props.file.type} onRename={this.onRename.bind(this)} is_renaming={this.state.is_renaming} />
+3 -4
View File
@@ -54,10 +54,9 @@ export class ViewerPage extends React.Component {
const data_fetch = (app) => {
if(app === 'editor'){
Files.cat(this.state.path).then((content) => {
this.setState({content: content || "", loading: false});
this.setState({content: content, loading: false});
}).catch(err => {
if(err && err.code === 'CANCELLED'){ return; }
if(err.code === 'BINARY_FILE'){
if(err && err.code === 'BINARY_FILE'){
this.setState({opener: 'download', loading: false});
}else{
notify.send(err, 'error');
@@ -135,7 +134,7 @@ export class ViewerPage extends React.Component {
needSaving={this.state.needSaving}
isSaving={this.state.isSaving}
onSave={this.save.bind(this)}
content={this.state.content}
content={this.state.content || ""}
url={this.state.url}
filename={this.state.filename}/>
</NgIf>
+5 -1
View File
@@ -44,7 +44,7 @@
/* Highlight Theme */
.cm-s-default .cm-header {color: #3E7AA6; font-size: 1.15em;}
.cm-s-default .cm-keyword {color: #3E7AA6;}
.cm-s-default .cm-header.cm-org-level-star{color: #6f6f6f; vertical-align: text-top;}
.cm-s-default .cm-header.cm-org-level-star{color: #6f6f6f;}
.cm-s-default .cm-header.cm-org-todo{color: #FF8355;}
.cm-s-default .cm-header.cm-org-done{color: #3BB27C;}
.cm-s-default .cm-link{color: #555!important;}
@@ -54,6 +54,10 @@
.cm-s-default .cm-string, .cm-s-default .cm-string-2{ color: #c41a16; }
.cm-s-default .cm-def { color: rgb(68, 85, 136); }
.CodeMirror-gutters{
box-shadow: 0px 0px 2px rgba(0,0,0,0.1);
}
.CodeMirror-foldmarker{
padding-left: 5px;
color: var(--color);