Introduction
I recently uploaded a file to a Nextcloud instance. After the upload, I saw that the file timestamp in Nextcloud is already some months old. But I just uploaded it, right? I double checked and saw that it matches the one on my local file system. How is this possible? Is a web application really able to read the last modification timestamp of the file? Apparently. I was not aware of this. In this post, I explain how this works and why you might care about this.
Analysis
Create a file with a specific timestamp in the past like 2024-05-23 17:42
:
$ echo "This is an example file." > file.txt
$ touch -d "2024-05-23 17:42" file.txt
The timestamp was set correctly:
$ ls -l file.txt
-rw------- 1 emanuel emanuel 25 May 23 17:42 file.txt
$ stat file.txt
[...]
Modify: 2024-05-23 17:42:00.000000000 +0200
[...]
When this file is uploaded to Nextcloud, this timestamp is then shown in the application:
In the file upload request, there is a request header X-Oc-Mtime
which’s name
and value look like this is used to send the timestamp in the Unix timestamp
format to the server:
PUT /remote.php/dav/files/emanuel/fileupload/file.txt HTTP/1.1
Host: nextcloud.example.net
Cookies: [...]
X-Oc-Mtime: 1716478920
[...]
This is an example file.
Indeed. Decoding the Unix timestamp shows that the value from the header matches the file modification timestamp:
$ date -d @1716478920
Thu May 23 05:42:00 PM CEST 2024
After some searching, I saw the JavaScript File API has a lastModified
property to read the last modification time of files 1:
Example
The API description also provides a ready-to-use example. Here’s a slightly
simplified version that just prints the timestamps of the selected files by
using the lastModified
property of the file object:
<input type="file" id="file-picker" name="fileList" multiple />
<pre id="output"></pre>
<script>
const output = document.getElementById("output");
const filePicker = document.getElementById("file-picker");
filePicker.addEventListener("change", (event) => {
const files = event.target.files;
const now = new Date();
output.textContent = "";
for (const file of files) {
const date = new Date(file.lastModified).toISOString();
output.textContent += `${file.name}: Last modified on ${date}.\n`;
}
});
</script>
This simple page can indeed read the timestamp of a selected file using JavaScript:
An application could then send this information along with the file content to the server during the file upload request, like Nextcloud does.
Timestamps in File Downloads
When the server has the timestamp, this information can of course also be shared when downloading the file again.
For this, the HTTP response header Last-Modified
can be used 2:
If and how this is done depends on the application of course.
Nextcloud’s Implementation
Here’s how Nextcloud is doing this.
Nextcloud implements this and sends the modification timestamp in the
Last-Modified
response header when a file is downloaded:
GET /remote.php/dav/files/emanuel/fileupload/file.txt HTTP/1.1
Host: nextcloud.example.net
Cookies: [...]
[...]
HTTP/1.1 200 OK
Content-Disposition: attachment; filename*=UTF-8''file.txt; filename="file.txt"
Content-Length: 25:
Content-Type: text/plain;charset=UTF-8
Date: Tue, 05 Nov 2024 21:56:07 GMT
Last-Modified: Thu, 23 May 2024 15:42:00 GMT
[...]
This is an example file.
Let’s check if the timestamp is shared if you share a file via a link:
Interestingly, when using Nextcloud’s feature to share a file via a link to, the timestamp is not disclosed:
GET /s/y3skAQyc3WfriMt/download/file.txt HTTP/1.1
Host: nextcloud.example.net
Cookie: [...]
[...]
HTTP/1.1 200 OK
Content-Disposition: attachment; filename*=UTF-8''file.txt; filename="file.txt"
Content-Transfer-Encoding: binary
Content-Type: text/plain;charset=UTF-8
Date: Tue, 05 Nov 2024 21:59:30 GMT
Expires: 0
Content-Length: 25
[...]
This is an example file.
Let’s see how it is implemented when you share an entire directory:
There, the file timestamp is again disclosed:
Strangely, the Last-Modified
header is not set in the response when the file is downloaded:
GET /s/icwsrroKZ3KE65f/download?path=%2F&files=file.txt&downloadStartSecret=beejjen7b0l HTTP/1.1
Host: nextcloud.example.net
Cookie: [...]
[...]
HTTP/1.1 200 OK
Content-Disposition: attachment; filename*=UTF-8''file.txt; filename="file.txt"
Content-Type: text/plain;charset=UTF-8
Date: Tue, 05 Nov 2024 22:05:23 GMT
[...]
This means, the timestamp is only shown in the web UI and not in the response header. This is also interesting to know.
Curl’s Remote-Time Feature
The -R
/--remote-time
option of curl can be used to directly set the local
file timestamp of downloaded files to the one received by the server 3:
$ man curl
[...]
-R, --remote-time
Makes curl attempt to figure out the timestamp of the remote file
that is getting downloaded, and if that is available make the lo‐
cal file get that same timestamp.
[...]
Downloading a resource via curl and the -R
option:
$ curl -sv -R -o example https://example.net/
* Host example.net:443 was resolved.
* IPv6: 2606:2800:21f:cb07:6820:80da:af6b:8b2c
[...]
> GET / HTTP/2
> Host: example.net
> User-Agent: curl/8.10.1
> Accept: */*
[...]
< HTTP/2 200
< content-type: text/html; charset=UTF-8
< date: Tue, 05 Nov 2024 22:23:05 GMT
< last-modified: Thu, 17 Oct 2019 07:18:26 GMT
[...]
The last-modified
header tells us that the remote timestamp is
Thu, 17 Oct 2019
.
The local file has now the same timestamp as the remote file:
$ ls -l example
-rw------- 1 emanuel emanuel 1256 Oct 17 2019 example
$ stat example
[...]
Access: 2019-10-17 09:18:26.000000000 +0200
Modify: 2019-10-17 09:18:26.000000000 +0200
Change: 2024-11-05 22:23:05.894429827 +0100
Birth: 2024-11-05 22:23:05.894429827 +0100
Takeaway
My takeaway is that every time you upload a file to a website, the website can also see the file timestamp and later share this information. Depending on the file or website, you may not want to leak this information for privacy reasons.
-
Mozilla Developer Network Documentation, File API: https://developer.mozilla.org/en-US/docs/Web/API/File/lastModified ↩︎
-
Mozilla Developer Network Documentation, HTTP Headers, Last-Modified: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified ↩︎
-
curl manpage,
-R
option: https://curl.se/docs/manpage.html#-R ↩︎