@@ -82,6 +82,8 @@ function my_devs(&$devs,$name,$menu) {
8282
8383function 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
150152if ($ 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">< ' .$ folder .'></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-
194259if ($ 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> ' ;
195260echo 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