mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2026-10-19 16:14:39 +00:00
bugfix (atomic update): update to indexDB weren't atomic causing out
of sync issues
This commit is contained in:
+65
-87
@@ -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
@@ -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
@@ -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(){
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user