Overview
This single page React
web app shows directory listing on
a webserver - in this example the ./apk
subdirectory.
The access URL would thus be something like
Whenever any new files are added, shortly after they
arrive in the directory, they will appear on the webpage listing.
Serverside (Apache
)
Virtual host configuration
In the <VirtualHost *:443 >
stanza of the Apache
configuration:
< Location "/apk" >
DirectoryIndex index.html
Options ExecCGI
</ Location >
Script to run as CGI in the directory:
This script index.cgi
will be accessed via an AJAX call
in the React app using the following relative URL:
./?listing
In the script, if the param listing
is supplied,
a JSON output of .apk
files is produced, otherwise the browser is redirected to the
React
web app’s entry point, ./index.html
.
#!/usr/bin/env bash
if [[ " $QUERY_STRING " == "listing" ]] ; then
echo "Access-Control-Allow-Origin: *"
echo "Content-Type: application/json"
echo
echo -n "["
started = ""
for i in * .apk; do
[ -z $started ] && started = 1 || echo ','
echo -n '{"file":"' $i '",'
echo -n '"ts":' $( stat -c %Y $i )
echo -n "}"
done
echo "]"
exit 0
fi
echo "Location: ./index.html"
echo
exit 0
Clientside React
app
Important configuration settings
In package.json
, the following setting is required,
to ensure that the React
app will run correctly from a
subdirectory.
"homepage" : "./apk"
Basis React
app
Could look something like this:
import ' ./App.css '
import { useCallback , useEffect , useRef , useState } from " react "
import Listing from " ./Listing " ;
const URL = ' ./index.cgi?listing '
const INTERVAL = 35 * 1000 /* update interval in msecs, e.g. every 35 secs */
const App = () => {
const [ files , setFiles ] = useState ([])
const [ lastFetch , setLastFetch ] = useState ( null )
const [ newSince , setNewSince ] = useState ([])
const mountedRef = useRef ( false )
const firstAccessRef = useRef ( + new Date ())
const doFetch = useCallback (() => {
if ( ! mountedRef . current ) return
fetch ( URL )
. then ( r => r . json ())
. then ( data => {
setLastFetch ( + new Date ())
data . sort (( a , b ) => a . ts < b . ts ? 1 : - 1 )
if ( mountedRef . current ) {
setFiles ( data )
}
if ( firstAccessRef && data ) {
let _newSince = data . filter (({ ts }) => (
ts * 1000 > firstAccessRef . current
))
if ( _newSince . length && newSince . length === 0 ) {
return setNewSince ( _newSince )
}
_newSince . map (({ ts , file }) => {
return setNewSince ( prev => {
let i
i = prev . findIndex ( e => (
e . ts === ts && e . file === file )
)
if ( i >= 0 ) return prev
i = prev . findIndex ( e => (
e . ts !== ts && e . file === file )
)
if ( i >= 0 ) {
prev = prev . splice ( i , 1 )
return [... prev , { ts , file }]
}
return prev . concat ({ ts , file })
})
})
}
})
}, [ newSince ])
useEffect (() => {
const plural = newSince . length > 1
const isAre = plural ? ' are ' : ' is '
if ( newSince . length ) {
console . log ( `There ${ isAre } ${ newSince . length } new or updated `
+ `file ${ plural ? ' s ' : '' } since page was loaded:` )
console . table ( newSince )
}
}, [ newSince ])
useEffect (() => {
mountedRef . current = true
doFetch ()
return () => mountedRef . current = false
}, [ doFetch ])
/* cf https://usehooks.com/useWindowSize/ */
const useWindowSize = () => {
const [ windowSize , setWindowSize ] = useState ({
width : undefined ,
height : undefined ,
});
useEffect (() => {
const handleResize = () => {
setWindowSize ({
width : window . innerWidth ,
height : window . innerHeight ,
});
}
window . addEventListener ( ' resize ' , handleResize );
handleResize ();
return () => window . removeEventListener (
' resize ' , handleResize
)
}, [])
return windowSize
}
/* cf
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
*/
const useInterval = ( callback , delay ) => {
const savedCallback = useRef ()
useEffect (() => {
savedCallback . current = callback
}, [ callback ])
useEffect (() => {
const tick = () => {
savedCallback . current ()
}
if ( delay !== null ) {
let id = setInterval ( tick , delay )
return () => clearInterval ( id )
}
}, [ delay ])
}
useInterval (() => {
doFetch ()
}, INTERVAL )
const { width } = useWindowSize ()
/* small devices: change flex direction to column */
let direction = width > 1024 ? ' row ' : ' column '
return (
< div
className = "App"
style = { {
backgroundColor : ' rgba(128,63,63,0.5) ' ,
minHeight : ' 99vh ' ,
} }
>
< header >
< h1 >< code > .apk</ code > Files for Download</ h1 >
</ header >
< Listing
direction = { direction }
files = { files }
newSince = { newSince }
/>
< footer >
< p > last checked: < code >
{ ( new Date ( lastFetch )). toISOString () }
</ code ></ p >
</ footer >
</ div >
)
}
export default App