Repository: mbrubeck/agate (language: rust)

Issue:
Add directory listing
When having a gemlog, it could be nice to be able to just link to a folder containing the articles and having the server serve the list of articles. 

Looking at the [Molly Brown server](https://tildegit.org/solderpunk/molly-brown), the feature can become quite complex. I think just allowing to display directories is enough. The users can sort the files by adding the date at the beginning of the filename for example.
For security reasons, maybe there should be a way to enable the directory listing per directory.


Discussion / hints:
The usually hidden files on Linux (i.e. files starting with a dot) should probably not be displayed. Then the listing could be enabled like in gitweb: If a file named e.g. `.directory-listing-ok` exists (its content does not matter, this is the "check file"), a directory listing is displayed if no `index.gmi` file exists.
1. Should this apply to subdirectories? (would probably bring performance penalties and may not be expected by users)
2. Should there be a command line option to allow listings in all directories?
3. Should the filename for the check file be configurable? (could be implemented as an optional value for 2.)

Fix patch:
diff --git a/README.md b/README.md
index 376cf70..dbbbbaf 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ agate --content path/to/content/ \
 
 All of the command-line arguments are optional.  Run `agate --help` to see the default values used when arguments are omitted.
 
-When a client requests the URL `gemini://example.com/foo/bar`, Agate will respond with the file at `path/to/content/foo/bar`.  If there is a directory at that path, Agate will look for a file named `index.gmi` inside that directory.
+When a client requests the URL `gemini://example.com/foo/bar`, Agate will respond with the file at `path/to/content/foo/bar`.  If there is a directory at that path, Agate will look for a file named `index.gmi` inside that directory. If there is no such file, but a file named `.directory-listing-ok` exists inside that directory, a basic directory listing is displayed. Files whose name starts with a dot (e.g. `.hidden`) are omitted from the list.
 
 [Gemini]: https://gemini.circumlunar.space/
 [Rust]: https://www.rust-lang.org/
diff --git a/src/main.rs b/src/main.rs
index 71eb835..461f858 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -188,6 +188,34 @@ async fn send_response<W: Write + Unpin>(url: Url, stream: &mut W) -> Result {
             // if the path ends with a slash or the path is empty, the links will work the same
             // without a redirect
             path.push("index.gmi");
+            if path.exists() {
+                // index file exists
+            } else if path.with_file_name(".directory-listing-ok").exists() {
+                // no index file, but directory listing allowed
+                path.pop();
+                log::info!("Listing directory {:?}", path);
+                let entries = std::fs::read_dir(path)?;
+                let listing = entries
+                    .filter(Result::is_ok)
+                    .map(Result::unwrap)
+                    .map(|entry| {
+                        // transform filenames into gemini link lines
+                        let mut name = String::from("=> ");
+                        name += &entry.file_name().to_string_lossy();
+                        if entry.path().is_dir() {
+                            // to avoid redirects link to directories with a trailing slash
+                            name += "/";
+                        }
+                        name + "\n"
+                    })
+                    // filter out files starting with a dot
+                    .filter(|entry| !entry.starts_with("=> ."))
+                    .collect::<String>();
+
+                respond(stream, "20", &["text/gemini"]).await?;
+                stream.write_all(listing.as_bytes()).await?;
+                return Ok(());
+            }
         } else {
             // if client is not redirected, links may not work as expected without trailing slash
             return respond(stream, "31", &[url.as_str(), "/"]).await;


Test patch: