diff --git a/common/spaces/datastore_space_primitives.ts b/common/spaces/datastore_space_primitives.ts index 046de0d..3880085 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 5fb24a3..6517d1f 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 1b2463a..8f1094a 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 98fdf74..8b2931c 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 ad5020d..50904c3 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 9836dce..8f29ee5 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 e4bc6a3..e36cb1a 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 b5ff6c4..2eab2dc 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 7aeff23..a674335 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 19f9593..1870eed 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 7362430..6f05e50 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 5864896..e1b1c3c 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 20f51c5..e930721 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 1ba636d..0a9f0a8 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 c382c46..8b38850 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 882a720..b33691c 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