Skip to content

Commit 9bbfe11

Browse files
committed
Merge remote-tracking branches 'origin/fix-issue-2487', 'origin/fix-issue-2488', 'origin/fix-issue-2493' and 'origin/fix-issue-2495' into baseline-all-other-fixes
4 parents b6f3080 + 1acc6cd + aa10dc9 + f23d234 commit 9bbfe11

6 files changed

Lines changed: 261 additions & 81 deletions

File tree

emhttp/plugins/dynamix/Browse.page

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ function doJobs(title) {
320320
},
321321
"_(Delete)_": function(){
322322
let row = [];
323-
dfm.window.find('i[id^="queue_"]').each(function(){if ($(this).hasClass('fa-check-square-o')) row.push((($(this).prop('id').split('_')[1]-1)*9)+1);});
323+
dfm.window.find('i[id^="queue_"]').each(function(){if ($(this).hasClass('fa-check-square-o')) row.push($(this).prop('id').split('_')[1]);});
324324
$.post('/webGui/include/Control.php',{mode:'undo',row:row.join(',')},function(queue){
325325
$.post('/webGui/include/Control.php',{mode:'jobs'},function(jobs){
326326
$('#dfm_jobs').html(jobs);
@@ -882,15 +882,22 @@ function doActions(action, title) {
882882
setTimeout(function(){if (dfm.window.find('#dfm_target').length) dfm.window.find('#dfm_target').focus().click(); else $('.ui-dfm .ui-dialog-buttonset button:eq(0)').focus();});
883883
}
884884

885-
function stopUpload(file,error) {
885+
function stopUpload(file,error,errorType) {
886886
window.onbeforeunload = null;
887+
currentXhr = null;
887888
$.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(dfm_htmlspecialchars(file))});
888889
$('#dfm_uploadButton').val("_(Upload)_").prop('onclick',null).off('click').click(function(){$('#dfm_upload').click();});
889890
$('#dfm_uploadStatus').html('');
890891
$('#dfm_upload').val('');
891892
dfm.running = false;
892893
loadList();
893-
if (error) setTimeout(function(){swal({title:"_(Upload Error)_",text:"_(File is removed)_",html:true,confirmButtonText:"_(Ok)_"});},200);
894+
if (error) {
895+
var message = "_(File is removed)_";
896+
if (errorType === 'timeout') message += "<br><br>_(Upload timed out. Please check your network connection and try again.)_";
897+
else if (errorType === 'network') message += "<br><br>_(Network error occurred. Please check your connection and try again.)_";
898+
else if (errorType && errorType.indexOf('http') === 0) message += "<br><br>_(HTTP error: )_" + errorType.substring(5);
899+
setTimeout(function(){swal({title:"_(Upload Error)_",text:message,html:true,confirmButtonText:"_(Ok)_"});},200);
900+
}
894901
}
895902

896903
function downloadFile(source) {
@@ -905,41 +912,89 @@ function downloadFile(source) {
905912

906913
function uploadFile(files,index,start,time) {
907914
var file = files[index];
908-
var slice = 2097152; // 2M
915+
var slice = 20971520; // 20MB chunks - no Base64 overhead, raw binary
909916
var next = start + slice;
910917
var blob = file.slice(start, next);
911-
reader.onloadend = function(e){
912-
if (e.target.readyState !== FileReader.DONE) return;
913-
$.post('/webGui/include/Control.php',{mode:'upload',file:encodeURIComponent(dir+'/'+dfm_htmlspecialchars(file.name)),start:start,data:window.btoa(e.target.result),cancel:cancel},function(reply){
914-
if (reply == 'stop') {stopUpload(file.name); return;}
915-
if (reply == 'error') {stopUpload(file.name,true); return;}
916-
if (next < file.size) {
917-
var total = 0;
918-
for (var i=0,f; f=files[i]; i++) {
919-
if (i < index) start += f.size;
920-
total += f.size;
921-
}
922-
const d = new Date();
923-
var speed = autoscale(((start + slice) * 8) / (d.getTime() - time));
924-
var percent = Math.floor((start + slice) / total * 100);
925-
$('#dfm_uploadStatus').html("_(Uploading)_: <span class='dfm_percent'>"+percent+"%</span><span class='dfm_speed'>Speed: "+speed+"</span><span class='orange-text'> ["+(index+1)+'/'+files.length+']&nbsp;&nbsp;'+file.name+"</span>");
926-
uploadFile(files,index,next,time);
927-
} else if (index < files.length-1) {
928-
uploadFile(files,index+1,0,time);
929-
} else {stopUpload(file.name); return;}
918+
919+
var xhr = new XMLHttpRequest();
920+
currentXhr = xhr; // Store for abort capability
921+
var filePath = dir.replace(/\/+$/, '') + '/' + dfm_htmlspecialchars(file.name);
922+
var url = '/webGui/include/Control.php?mode=upload&file=' + encodeURIComponent(filePath) + '&start=' + start + '&cancel=' + cancel;
923+
xhr.open('POST', url, true);
924+
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
925+
xhr.setRequestHeader('X-CSRF-Token', '<?=$var['csrf_token']?>');
926+
xhr.timeout = Math.max(600000, slice / 1024 * 60); // ~1 minute per MB, minimum 10 minutes
927+
928+
xhr.onload = function() {
929+
if (xhr.status < 200 || xhr.status >= 300) {
930+
stopUpload(file.name, true, 'http:' + xhr.status);
931+
return;
932+
}
933+
var reply = xhr.responseText;
934+
if (reply == 'stop') {stopUpload(file.name); return;}
935+
if (reply.indexOf('error') === 0) {
936+
console.error('Upload error:', reply);
937+
stopUpload(file.name,true);
938+
return;
939+
}
940+
if (next < file.size) {
941+
var total = 0;
942+
var completed = 0;
943+
for (var i=0,f; f=files[i]; i++) {
944+
if (i < index) completed += f.size;
945+
total += f.size;
946+
}
947+
const d = new Date();
948+
var bytesTransferred = completed + next;
949+
var elapsedSeconds = (d.getTime() - time) / 1000;
950+
var speed = autoscale(bytesTransferred / elapsedSeconds);
951+
var percent = Math.floor(bytesTransferred / total * 100);
952+
$('#dfm_uploadStatus').html("_(Uploading)_: <span class='dfm_percent'>"+percent+"%</span><span class='dfm_speed'>Speed: "+speed+"</span><span class='orange-text'> ["+(index+1)+'/'+files.length+']&nbsp;&nbsp;'+file.name+"</span>");
953+
uploadFile(files,index,next,time);
954+
} else if (index < files.length-1) {
955+
// Clean up temp file for completed upload before starting next file
956+
$.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(dfm_htmlspecialchars(file.name))});
957+
uploadFile(files,index+1,0,time);
958+
} else {stopUpload(file.name); return;}
959+
};
960+
961+
xhr.onabort = function() {
962+
// User cancelled upload - trigger deletion via cancel=1 parameter
963+
$.post('/webGui/include/Control.php', {
964+
mode: 'upload',
965+
file: filePath,
966+
start: 0,
967+
cancel: 1
968+
}).always(function() {
969+
// Cleanup UI regardless of POST success/failure
970+
stopUpload(file.name, false);
930971
});
931972
};
932-
reader.readAsBinaryString(blob);
973+
974+
xhr.onerror = function() {
975+
// Don't show error if it was a user cancel
976+
if (cancel === 1) return;
977+
stopUpload(file.name, true, 'network');
978+
};
979+
980+
xhr.ontimeout = function() {
981+
stopUpload(file.name, true, 'timeout');
982+
};
983+
984+
xhr.send(blob);
933985
}
934986

935-
var reader = {};
936987
var cancel = 0;
988+
var currentXhr = null;
937989

938990
function startUpload(files) {
939991
if (files.length == 0) return;
940-
reader = new FileReader();
992+
cancel = 0; // Reset cancel flag
941993
window.onbeforeunload = function(e){return '';};
942-
$('#dfm_uploadButton').val("_(Cancel)_").prop('onclick',null).off('click').click(function(){cancel=1;});
994+
$('#dfm_uploadButton').val("_(Cancel)_").prop('onclick',null).off('click').click(function(){
995+
cancel=1;
996+
if (currentXhr) currentXhr.abort();
997+
});
943998
dfm.running = true;
944999
const d = new Date();
9451000
uploadFile(files,0,0,d.getTime());

emhttp/plugins/dynamix/include/Browse.php

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ function my_devs(&$devs,$name,$menu) {
8282

8383
function icon_class($ext) {
8484
switch ($ext) {
85+
case 'broken-symlink':
86+
return 'fa fa-chain-broken red-text';
8587
case '3gp': case 'asf': case 'avi': case 'f4v': case 'flv': case 'm4v': case 'mkv': case 'mov': case 'mp4': case 'mpeg': case 'mpg': case 'm2ts': case 'ogm': case 'ogv': case 'vob': case 'webm': case 'wmv':
8688
return 'fa fa-film';
8789
case '7z': case 'bz2': case 'gz': case 'rar': case 'tar': case 'xz': case 'zip':
@@ -149,48 +151,111 @@ function icon_class($ext) {
149151

150152
if ($user ) {
151153
exec("shopt -s dotglob;getfattr --no-dereference --absolute-names -n system.LOCATIONS ".escapeshellarg($dir)."/* 2>/dev/null",$tmp);
152-
for ($i = 0; $i < count($tmp); $i+=3) $set[basename($tmp[$i])] = explode('"',$tmp[$i+1])[1];
154+
// Decode octal escapes from getfattr output to match actual filenames
155+
// Reason: "getfattr" outputs \012 (newline) but the below "find" returns actual newline character
156+
for ($i = 0; $i < count($tmp); $i+=3) {
157+
// Check bounds: if getfattr fails for a file, we might not have all 3 lines
158+
if (!isset($tmp[$i+1])) break;
159+
$filename = preg_replace_callback('/\\\\([0-7]{3})/', function($m) { return chr(octdec($m[1])); }, $tmp[$i]);
160+
$set[basename($filename)] = explode('"',$tmp[$i+1])[1];
161+
}
153162
unset($tmp);
154163
}
155164

156-
$stat = popen("shopt -s dotglob;stat -L -c'%F|%U|%A|%s|%Y|%n' ".escapeshellarg($dir)."/* 2>/dev/null",'r');
165+
// Get directory listing with stat info NULL-separated to support newlines in file/dir names
166+
// Two separate finds: working symlinks with target info, broken symlinks marked as such
167+
// Format: 7 fields per entry separated by \0: type\0owner\0perms\0size\0timestamp\0name\0symlinkTarget\0
168+
$cmd = <<<'BASH'
169+
cd %s && {
170+
find . -maxdepth 1 -mindepth 1 ! -xtype l -printf '%%y\0%%u\0%%M\0%%s\0%%T@\0%%p\0%%l\0' 2>/dev/null
171+
find . -maxdepth 1 -mindepth 1 -xtype l -printf 'broken\0%%u\0%%M\0%%s\0%%T@\0%%p\0%%l\0' 2>/dev/null
172+
}
173+
BASH;
174+
$stat = popen(sprintf($cmd, escapeshellarg($dir)), 'r');
175+
176+
// Read all output and split by \0 into array
177+
$all_output = stream_get_contents($stat);
178+
pclose($stat);
179+
$fields_array = explode("\0", $all_output);
180+
181+
// Process in groups of 7 fields per entry
182+
for ($i = 0; $i + 7 <= count($fields_array); $i += 7) {
183+
$fields = array_slice($fields_array, $i, 7);
184+
[$type,$owner,$perm,$size,$time,$name,$target] = $fields;
185+
$time = (int)$time;
186+
$name = $dir.'/'.substr($name, 2); // Remove './' prefix from find output
187+
188+
// Determine device name for LOCATION column
189+
// For symlinks with absolute targets, use the target path to determine the device
190+
// For everything else, use the source path
191+
if ($target && $target[0] == '/') {
192+
193+
// Absolute symlink: extract device from target path
194+
// Example: /mnt/disk2/foo/bar -> dev[2] = 'disk2'
195+
$dev = explode('/', $target, 5);
196+
$dev_name = $dev[2] ?? '';
197+
198+
} else {
199+
200+
// Regular file/folder or relative symlink: extract from source path
201+
// Example: /mnt/disk1/sharename/foo -> dev[3] = 'sharename', dev[2] = 'disk1'
202+
$dev = explode('/', $name, 5);
203+
$dev_name = $dev[3] ?? $dev[2];
204+
205+
}
206+
207+
// Build device list for LOCATION column
208+
// In user share: get device list from xattr (system.LOCATIONS) or share config
209+
if ($user) {
210+
$devs_value = $set[basename($name)] ?? $shares[$dev_name]['cachePool'] ?? '';
211+
212+
// On direct disk path:
213+
} else {
214+
215+
// For absolute symlinks: use the target's device name
216+
if ($target && $target[0] == '/') {
217+
$devs_value = $dev_name;
218+
219+
// For regular files/folders: use current device name like disk1, boot, etc.
220+
} else {
221+
$devs_value = $lock;
222+
}
223+
224+
}
225+
$devs = explode(',', $devs_value);
157226

158-
while (($row = fgets($stat)) !== false) {
159-
[$type,$owner,$perm,$size,$time,$name] = explode('|',rtrim($row,"\n"),6);
160-
$dev = explode('/', $name, 5);
161-
$devs = explode(',', $user ? $set[basename($name)] ?? $shares[$dev[3]]['cachePool'] ?? '' : $lock);
162227
$objs++;
163228
$text = [];
164-
if ($type[0] == 'd') {
229+
if ($type == 'd') {
165230
$text[] = '<tr><td><i id="check_'.$objs.'" class="fa fa-fw fa-square-o" onclick="selectOne(this.id)"></i></td>';
166231
$text[] = '<td data=""><i class="fa fa-folder-o"></i></td>';
167-
$text[] = '<td><a id="name_'.$objs.'" oncontextmenu="folderContextMenu(this.id,\'right\');return false" href="/'.$path.'?dir='.rawurlencode(htmlspecialchars($name)).'">'.htmlspecialchars(basename($name)).'</a></td>';
232+
// nl2br() is used to preserve newlines in file/dir names
233+
$text[] = '<td><a id="name_'.$objs.'" oncontextmenu="folderContextMenu(this.id,\'right\');return false" href="/'.$path.'?dir='.rawurlencode(htmlspecialchars($name)).'">'.nl2br(htmlspecialchars(basename($name))).'</a></td>';
168234
$text[] = '<td id="owner_'.$objs.'">'.$owner.'</td>';
169235
$text[] = '<td id="perm_'.$objs.'">'.$perm.'</td>';
170236
$text[] = '<td data="0">&lt;'.$folder.'&gt;</td>';
171237
$text[] = '<td data="'.$time.'"><span class="my_time">'.my_time($time,$fmt).'</span><span class="my_age" style="display:none">'.my_age($time).'</span></td>';
172-
$text[] = '<td class="loc">'.my_devs($devs,$dev[3]??$dev[2],'deviceFolderContextMenu').'</td>';
238+
$text[] = '<td class="loc">'.my_devs($devs,$dev_name,'deviceFolderContextMenu').'</td>';
173239
$text[] = '<td><i id="row_'.$objs.'" data="'.escapeQuote($name).'" type="d" class="fa fa-plus-square-o" onclick="folderContextMenu(this.id,\'both\')" oncontextmenu="folderContextMenu(this.id,\'both\');return false">...</i></td></tr>';
174240
$dirs[] = gzdeflate(implode($text));
175241
} else {
176-
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
242+
$is_broken = ($type == 'broken');
243+
$ext = $is_broken ? 'broken-symlink' : strtolower(pathinfo($name, PATHINFO_EXTENSION));
177244
$tag = count($devs) > 1 ? 'warning' : '';
178245
$text[] = '<tr><td><i id="check_'.$objs.'" class="fa fa-fw fa-square-o" onclick="selectOne(this.id)"></i></td>';
179246
$text[] = '<td class="ext" data="'.$ext.'"><i class="'.icon_class($ext).'"></i></td>';
180-
$text[] = '<td id="name_'.$objs.'" class="'.$tag.'" onclick="fileEdit(this.id)" oncontextmenu="fileContextMenu(this.id,\'right\');return false">'.htmlspecialchars(basename($name)).'</td>';
247+
$text[] = '<td id="name_'.$objs.'" class="'.$tag.'"'.($is_broken ? '' : ' onclick="fileEdit(this.id)"').' oncontextmenu="fileContextMenu(this.id,\'right\');return false">'.nl2br(htmlspecialchars(basename($name))).'</td>';
181248
$text[] = '<td id="owner_'.$objs.'" class="'.$tag.'">'.$owner.'</td>';
182249
$text[] = '<td id="perm_'.$objs.'" class="'.$tag.'">'.$perm.'</td>';
183250
$text[] = '<td data="'.$size.'" class="'.$tag.'">'.my_scale($size,$unit).' '.$unit.'</td>';
184251
$text[] = '<td data="'.$time.'" class="'.$tag.'"><span class="my_time">'.my_time($time,$fmt).'</span><span class="my_age" style="display:none">'.my_age($time).'</span></td>';
185-
$text[] = '<td class="loc '.$tag.'">'.my_devs($devs,$dev[3]??$dev[2],'deviceFileContextMenu').'</td>';
252+
$text[] = '<td class="loc '.$tag.'">'.my_devs($devs,$dev_name,'deviceFileContextMenu').'</td>';
186253
$text[] = '<td><i id="row_'.$objs.'" data="'.escapeQuote($name).'" type="f" class="fa fa-plus-square-o" onclick="fileContextMenu(this.id,\'both\')" oncontextmenu="fileContextMenu(this.id,\'both\');return false">...</i></td></tr>';
187254
$files[] = gzdeflate(implode($text));
188255
$total += $size;
189256
}
190257
}
191258

192-
pclose($stat);
193-
194259
if ($link = parent_link()) echo '<tbody class="tablesorter-infoOnly"><tr><td></td><td><i class="fa fa-folder-open-o"></i></td><td>',$link,'</td><td colspan="6"></td></tr></tbody>';
195260
echo write($dirs),write($files),'<tfoot><tr><td></td><td></td><td colspan="7">',add($objs,'object'),': ',add($dirs,'director','y','ies'),', ',add($files,'file'),' (',my_scale($total,$unit),' ',$unit,' ',_('total'),')</td></tr></tfoot>';
196261
?>

0 commit comments

Comments
 (0)