diff --git a/common/spaces/datastore_space_primitives.ts b/common/spaces/datastore_space_primitives.ts index 046de0d2..38800859 100644 --- a/common/spaces/datastore_space_primitives.ts +++ b/common/spaces/datastore_space_primitives.ts @@ -48,6 +48,7 @@ export class DataStoreSpacePrimitives implements SpacePrimitives { ): Promise { const meta: FileMeta = { name, + created: suggestedMeta?.lastModified || Date.now(), lastModified: suggestedMeta?.lastModified || Date.now(), contentType: mime.getType(name) || "application/octet-stream", size: data.byteLength, diff --git a/common/spaces/deno_kv_space_primitives.ts b/common/spaces/deno_kv_space_primitives.ts index 5fb24a30..6517d1f9 100644 --- a/common/spaces/deno_kv_space_primitives.ts +++ b/common/spaces/deno_kv_space_primitives.ts @@ -57,6 +57,7 @@ export class DenoKVSpacePrimitives implements SpacePrimitives { ): Promise { const meta: FileMeta = { name, + created: suggestedMeta?.created || Date.now(), lastModified: suggestedMeta?.lastModified || Date.now(), contentType: mime.getType(name) || "application/octet-stream", size: data.byteLength, diff --git a/common/spaces/disk_space_primitives.ts b/common/spaces/disk_space_primitives.ts index 1b2463a3..8f1094ab 100644 --- a/common/spaces/disk_space_primitives.ts +++ b/common/spaces/disk_space_primitives.ts @@ -54,6 +54,7 @@ export class DiskSpacePrimitives implements SpacePrimitives { data, meta: { name: name, + created: s.birthtime!.getTime(), lastModified: s.mtime!.getTime(), perm: "rw", size: s.size, @@ -108,6 +109,7 @@ export class DiskSpacePrimitives implements SpacePrimitives { name: name, size: s.size, contentType: lookupContentType(name), + created: s.birthtime!.getTime(), lastModified: s.mtime!.getTime(), perm: "rw", }; @@ -145,6 +147,7 @@ export class DiskSpacePrimitives implements SpacePrimitives { } allFiles.push({ name: normalizeForwardSlashPath(name), + created: s.birthtime!.getTime(), lastModified: s.mtime!.getTime(), contentType: mime.getType(fullPath) || "application/octet-stream", size: s.size, diff --git a/common/spaces/http_space_primitives.ts b/common/spaces/http_space_primitives.ts index 98fdf744..8b2931cc 100644 --- a/common/spaces/http_space_primitives.ts +++ b/common/spaces/http_space_primitives.ts @@ -108,6 +108,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { "Content-Type": "application/octet-stream", }; if (meta) { + headers["X-Created"] = "" + meta.created; headers["X-Last-Modified"] = "" + meta.lastModified; headers["X-Perm"] = "" + meta.perm; } @@ -165,6 +166,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { ? +res.headers.get("X-Content-Length")! : +res.headers.get("Content-Length")!, contentType: res.headers.get("Content-type")!, + created: +(res.headers.get("X-Created") || "0"), lastModified: +(res.headers.get("X-Last-Modified") || "0"), perm: (res.headers.get("X-Permission") as "rw" | "ro") || "ro", }; diff --git a/plug-api/types.ts b/plug-api/types.ts index ad5020de..50904c35 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -1,5 +1,6 @@ export type FileMeta = { name: string; + created: number; lastModified: number; contentType: string; size: number; @@ -9,6 +10,7 @@ export type FileMeta = { export type PageMeta = { name: string; + created: number; lastModified: number; lastOpened?: number; perm: "ro" | "rw"; @@ -17,6 +19,7 @@ export type PageMeta = { export type AttachmentMeta = { name: string; contentType: string; + created: number; lastModified: number; size: number; perm: "ro" | "rw"; diff --git a/plugos/syscalls/fs.deno.ts b/plugos/syscalls/fs.deno.ts index 9836dce1..8f29ee51 100644 --- a/plugos/syscalls/fs.deno.ts +++ b/plugos/syscalls/fs.deno.ts @@ -34,6 +34,7 @@ export default function fileSystemSyscalls(root = "/"): SysCallMapping { const s = await Deno.stat(p); return { name: filePath, + created: s.birthtime!.getTime(), lastModified: s.mtime!.getTime(), contentType: mime.getType(filePath) || "application/octet-stream", size: s.size, @@ -56,6 +57,7 @@ export default function fileSystemSyscalls(root = "/"): SysCallMapping { const s = await Deno.stat(p); return { name: filePath, + created: s.birthtime!.getTime(), lastModified: s.mtime!.getTime(), contentType: mime.getType(filePath) || "application/octet-stream", size: s.size, @@ -84,6 +86,7 @@ export default function fileSystemSyscalls(root = "/"): SysCallMapping { const s = await Deno.stat(fullPath); allFiles.push({ name: fullPath.substring(dirPath.length + 1), + created: s.birthtime!.getTime(), lastModified: s.mtime!.getTime(), contentType: mime.getType(fullPath) || "application/octet-stream", size: s.size, diff --git a/plugs/federation/federation.ts b/plugs/federation/federation.ts index e4bc6a38..e36cb1a0 100644 --- a/plugs/federation/federation.ts +++ b/plugs/federation/federation.ts @@ -25,6 +25,7 @@ async function responseToFileMeta( : 0, contentType: r.headers.get("Content-type")!, perm, + created: +(r.headers.get("X-Created") || "0"), lastModified: +(r.headers.get("X-Last-Modified") || "0"), }; } @@ -147,6 +148,7 @@ function errorResult( meta: { name, contentType: "text/markdown", + created: 0, lastModified: 0, size: 0, perm: "ro", diff --git a/plugs/index/page.ts b/plugs/index/page.ts index b5ff6c4e..2eab2dca 100644 --- a/plugs/index/page.ts +++ b/plugs/index/page.ts @@ -8,7 +8,8 @@ import { indexObjects } from "./api.ts"; export type PageObject = ObjectValue< // The base is PageMeta, but we override lastModified to be a string - Omit & { + Omit, "created"> & { + created: string; // indexing it as a string lastModified: string; // indexing it as a string } & Record >; @@ -23,6 +24,7 @@ export async function indexPage({ name, tree }: IndexTreeEvent) { ref: name, tags: [], // will be overridden in a bit ...pageMeta, + created: new Date(pageMeta.created).toISOString(), lastModified: new Date(pageMeta.lastModified).toISOString(), }; diff --git a/plugs/search/search.ts b/plugs/search/search.ts index 7aeff23d..a6743357 100644 --- a/plugs/search/search.ts +++ b/plugs/search/search.ts @@ -73,6 +73,7 @@ export async function readFileSearch( name, contentType: "text/markdown", size: text.length, + created: 0, lastModified: 0, perm: "ro", }, @@ -91,6 +92,7 @@ export function getFileMetaSearch(name: string): FileMeta { name, contentType: "text/markdown", size: -1, + created: 0, lastModified: 0, perm: "ro", }; diff --git a/plugs/template/template.ts b/plugs/template/template.ts index 19f95930..1870eeda 100644 --- a/plugs/template/template.ts +++ b/plugs/template/template.ts @@ -40,6 +40,7 @@ export async function instantiateTemplateCommand() { const tempPageMeta: PageMeta = { name: "", + created: 0, lastModified: 0, perm: "rw", }; @@ -207,6 +208,7 @@ export async function dailyNoteCommand() { pageName, await replaceTemplateVars(dailyNoteTemplateText, { name: pageName, + created: 0, lastModified: 0, perm: "rw", }), @@ -251,6 +253,7 @@ export async function weeklyNoteCommand() { pageName, await replaceTemplateVars(weeklyNoteTemplateText, { name: pageName, + created: 0, lastModified: 0, perm: "rw", }), @@ -272,7 +275,8 @@ export async function insertTemplateText(cmdDef: any) { // Likely page not yet created pageMeta = { name: page, - lastModified: -1, + created: 0, + lastModified: 0, perm: "rw", }; } diff --git a/scripts/generate_fs_list.ts b/scripts/generate_fs_list.ts index 73624304..6f05e507 100644 --- a/scripts/generate_fs_list.ts +++ b/scripts/generate_fs_list.ts @@ -23,6 +23,7 @@ for await ( allFiles.push({ name: fullPath.substring(rootDir.length + 1), lastModified: lastModifiedTimestamp, + created: lastModifiedTimestamp, contentType: mime.getType(fullPath) || "application/octet-stream", size: s.size, perm: "rw", diff --git a/server/http_server.ts b/server/http_server.ts index 58648969..e1b1c3ce 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -540,6 +540,10 @@ export class HttpServer { "X-Last-Modified", "" + fileMeta.lastModified, ); + headers.set( + "X-Created", + "" + fileMeta.created, + ); headers.set("Cache-Control", "no-cache"); headers.set("X-Permission", fileMeta.perm); headers.set("X-Content-Length", "" + fileMeta.size); diff --git a/server/spaces/s3_space_primitives.ts b/server/spaces/s3_space_primitives.ts index 20f51c54..e9307217 100644 --- a/server/spaces/s3_space_primitives.ts +++ b/server/spaces/s3_space_primitives.ts @@ -5,6 +5,8 @@ import { SpacePrimitives } from "../../common/spaces/space_primitives.ts"; import { mime } from "../deps.ts"; import { FileMeta } from "$sb/types.ts"; +// TODO: IMPORTANT: This needs a different way to keep meta data (last modified and created dates) + export class S3SpacePrimitives implements SpacePrimitives { client: S3Client; constructor(options: ClientOptions) { @@ -27,6 +29,7 @@ export class S3SpacePrimitives implements SpacePrimitives { allFiles.push({ name: this.decodePath(obj.key), perm: "rw", + created: 0, lastModified: obj.lastModified.getTime(), contentType: mime.getType(obj.key) || "application/octet-stream", size: obj.size, @@ -46,6 +49,7 @@ export class S3SpacePrimitives implements SpacePrimitives { const meta: FileMeta = { name, perm: "rw", + created: 0, lastModified: new Date(obj.headers.get("Last-Modified")!).getTime(), contentType, size: parseInt(obj.headers.get("Content-Length")!), @@ -70,6 +74,8 @@ export class S3SpacePrimitives implements SpacePrimitives { return { name, perm: "rw", + // TODO: Created is not accurate + created: 0, lastModified: new Date(stat.lastModified).getTime(), size: stat.size, contentType: mime.getType(name) || "application/octet-stream", diff --git a/web/service_worker.ts b/web/service_worker.ts index 1ba636db..0a9f0a8f 100644 --- a/web/service_worker.ts +++ b/web/service_worker.ts @@ -138,6 +138,7 @@ async function handleLocalFileRequest( "Content-type": data.meta.contentType, "Content-Length": "" + data.meta.size, "X-Permission": data.meta.perm, + "X-Created": "" + data.meta.created, "X-Last-Modified": "" + data.meta.lastModified, }, }, diff --git a/website/API.md b/website/API.md index c382c468..8b388509 100644 --- a/website/API.md +++ b/website/API.md @@ -7,6 +7,7 @@ The API: * `GET /index.json` will return a full listing of all files in your space including metadata like when the file was last modified, as well as permissions. This is primarily used for sync purposes with the client. * `GET /*.*`: _Reads_ and returns the content of the file at the given path. This means that if you `GET /index.md` you will receive the content of your `index` page. If the the optional `X-Get-Meta` _request header_ is set, the server does not _need to_ return the body of the file (but it can). The `GET` _response_ will have a few additional SB-specific headers: * (optional) `X-Last-Modified` the last modified time of the file as a UNIX timestamp in ms since the epoch (as coming from `Data.now()`). This timestamp _has_ to match the `lastModified` listed for this file in `/index.json` otherwise syncing issues may occur. When this header is missing, frequent polling-based sync will be disabled for this file. + * (optional) `X-Created` the created time of the file as a UNIX timestamp in ms since the epoch (as coming from `Data.now()`). * (optional) `X-Permission`: either `rw` or `ro` which will change whether the editor opens in read-only or edit mode. When missing, `ro` is assumed. * (optional) `X-Content-Length`: which will be the same as `Content-Length` except if the request was sent with a `X-Get-Meta` header and the body is not returned (then `Content-Length` will be `0` and `X-Content-Length` will be the size of the file) * `PUT /*.*`: The same as `GET` except that it takes the body of the request and _writes_ it to a file. diff --git a/website/_headers b/website/_headers index 882a7200..b33691c8 100644 --- a/website/_headers +++ b/website/_headers @@ -1,5 +1,6 @@ /* X-Last-Modified: 12345 + X-Created: 12345 X-Permission: rw access-control-allow-headers: * access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS,HEAD