-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from relay-md/release/20240213
Release/20240213
- Loading branch information
Showing
6 changed files
with
582 additions
and
455 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { RequestUrlParam, RequestUrlResponse, requestUrl, Notice } from 'obsidian'; | ||
import RelayMdPLugin from './main' | ||
|
||
export interface FileUpload { | ||
filetype: string | ||
hash: string | ||
content?: ArrayBuffer | string | ||
byteLength: number | ||
expiration?: number | ||
url?: string | null | ||
} | ||
|
||
export interface FileDownload { | ||
filetype: string | ||
hash: string | ||
content?: ArrayBuffer | string | ||
byteLength: number | ||
expiration?: number | ||
url?: string | null | ||
} | ||
|
||
export type PostData = { | ||
files?: FileUpload[] | ||
filename?: string | ||
filetype?: string | ||
hash?: string | ||
byteLength?: number | ||
expiration?: number | ||
} | ||
|
||
export interface QueueItem { | ||
data: FileUpload | FileDownload | ||
callback: (url: string) => void | ||
} | ||
|
||
export class API { | ||
plugin: RelayMdPLugin | ||
uploadQueue: QueueItem[] | ||
|
||
constructor(plugin: RelayMdPLugin) { | ||
this.plugin = plugin | ||
this.uploadQueue = [] | ||
} | ||
|
||
async post(endpoint: string, body: string, retries = 1) { | ||
const options: RequestUrlParam = { | ||
url: this.plugin.settings.base_uri + endpoint, | ||
method: 'POST', | ||
headers: { | ||
'X-API-KEY': this.plugin.settings.api_key, | ||
}, | ||
body: body | ||
} | ||
const response: RequestUrlResponse = await requestUrl(options); | ||
if (response.json.error) { | ||
console.error("API server returned an error", response.json.error.message); | ||
new Notice("API returned an error: " + response.json.error.message); | ||
return null; | ||
} | ||
return response; | ||
} | ||
async postRaw(endpoint: string, body: ArrayBuffer, retries = 4) { | ||
const options: RequestUrlParam = { | ||
url: this.plugin.settings.base_uri + endpoint, | ||
method: 'POST', | ||
headers: { | ||
'X-API-KEY': this.plugin.settings.api_key, | ||
}, | ||
body: body | ||
} | ||
const response: RequestUrlResponse = await requestUrl(options); | ||
if (response.json.error) { | ||
console.error("API server returned an error", response.json.error.message); | ||
new Notice("API returned an error: " + response.json.error.message); | ||
return null; | ||
} | ||
return response; | ||
} | ||
|
||
async put(endpoint: string, body: string, retries = 4) { | ||
const options: RequestUrlParam = { | ||
url: this.plugin.settings.base_uri + endpoint, | ||
method: 'PUT', | ||
headers: { | ||
'X-API-KEY': this.plugin.settings.api_key, | ||
}, | ||
body: body | ||
} | ||
const response: RequestUrlResponse = await requestUrl(options); | ||
if (response.json.error) { | ||
console.error("API server returned an error", response.json.error.message); | ||
new Notice("API returned an error: " + response.json.error.message); | ||
return null; | ||
} | ||
return response; | ||
} | ||
async putRaw(endpoint: string, data?: PostData, retries = 1) { | ||
return null; | ||
} | ||
|
||
async get(endpoint: string, content_type: string = "application/json", retries: number = 1,): Promise<RequestUrlResponse | null> { | ||
const options: RequestUrlParam = { | ||
url: this.plugin.settings.base_uri + endpoint, | ||
method: 'GET', | ||
headers: { | ||
'X-API-KEY': this.plugin.settings.api_key, | ||
'Content-Type': content_type | ||
}, | ||
} | ||
const response: RequestUrlResponse = await requestUrl(options); | ||
if (content_type == "application/json") { | ||
if (response.json.error) { | ||
console.error("API server returned an error", response.json.error.message); | ||
new Notice("API returned an error: " + response.json.error.message); | ||
return null; | ||
} | ||
} | ||
return response; | ||
} | ||
async getRaw(endpoint: string, data: FileDownload, retries = 4) { | ||
return null; | ||
} | ||
|
||
// Queue | ||
async queueUpload(item: QueueItem) { | ||
return null; | ||
} | ||
async queueDownload(item: QueueItem) { | ||
return null; | ||
} | ||
async processQueue() { | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import { Notice, TFile, TFolder, normalizePath } from "obsidian" | ||
import RelayMdPLugin from './main' | ||
import { IEmbed, EmbedRepo } from './embed' | ||
import { API } from "./api" | ||
import { Utils } from "./utils" | ||
|
||
export interface IDocument { | ||
"relay-to": Array<string>; | ||
"relay-document": string; | ||
"relay-filename": string; | ||
"relay-title": string; | ||
checksum_sha256: string; | ||
embeds: Array<IEmbed>; | ||
filesize: number; | ||
} | ||
|
||
export class DocumentRepo { | ||
plugin: RelayMdPLugin | ||
api: API | ||
embed_repo: EmbedRepo | ||
|
||
constructor(plugin: RelayMdPLugin) { | ||
this.plugin = plugin; | ||
this.api = new API(this.plugin); | ||
this.embed_repo = new EmbedRepo(this.plugin); | ||
} | ||
|
||
async get_recent_documents() { | ||
const response = await this.api.get("/v1/docs") | ||
if (!response) return; | ||
response.json.result.map(async (item: IDocument) => { | ||
new Notice("Obtaining " + item["relay-filename"]); | ||
await this.load_document(item["relay-document"]); | ||
}) | ||
} | ||
|
||
async load_document(id: string) { | ||
const response = await this.api.get("/v1/doc/" + id) | ||
if (!response) return; | ||
const result = response.json.result; | ||
const embeds = result.embeds; | ||
if (embeds) { | ||
embeds.map((embed: IEmbed) => { | ||
this.embed_repo.load_embeds(embed, result); | ||
}); | ||
} | ||
this.load_document_body(result); | ||
} | ||
|
||
async load_document_body(result: IDocument) { | ||
const id = result["relay-document"]; | ||
const filename = result["relay-filename"]; | ||
const relay_to = result["relay-to"]; | ||
const remote_checksum = result["checksum_sha256"]; | ||
|
||
const response = await this.api.get("/v1/doc/" + id, "text/markdown"); | ||
if (!response) return; | ||
const body: string = response.text; | ||
|
||
// Let's first see if we maybe have the document already somewhere in the fault | ||
const located_documents = await this.locate_document(id); | ||
if (located_documents?.length) { | ||
located_documents.forEach(async (located_document: TFile) => { | ||
// Compare checksum | ||
const local_content = await this.plugin.app.vault.readBinary(located_document); | ||
const checksum = await Utils.calculateSHA256Checksum(local_content); | ||
if (checksum != remote_checksum) { | ||
this.upsert_document(located_document.path, body); | ||
} else { | ||
console.log("No change detected on " + located_document.path); | ||
} | ||
}) | ||
} else { | ||
// Loop through team/topics | ||
for (const to of relay_to) { | ||
const tos: Array<string> = to.split("@", 2); | ||
const team: string = tos[1]; | ||
const topic: string = tos[0]; | ||
let folder: string = this.plugin.settings.vault_base_folder + "/"; | ||
// We ignore the "_" team which is a "global" team | ||
if (team != "_") | ||
folder += team + "/"; | ||
folder += topic; | ||
const path = normalizePath(folder + "/" + filename); | ||
this.upsert_document(path, body); | ||
} | ||
} | ||
} | ||
|
||
async upsert_document(path: string, body: string) { | ||
Utils.make_directory_recursively(this.plugin.app, path); | ||
const fileRef = this.plugin.app.vault.getAbstractFileByPath(path); | ||
if (fileRef === undefined || fileRef === null) { | ||
await this.plugin.app.vault.create(path, body); | ||
new Notice('File ' + path + ' has been created!'); | ||
} else if (fileRef instanceof TFile) { | ||
await this.plugin.app.vault.modify(fileRef, body); | ||
new Notice('File ' + path + ' has been modified!'); | ||
} | ||
} | ||
|
||
async locate_document(document_id: string) { | ||
const files = this.plugin.app.vault.getMarkdownFiles(); | ||
let located_files = []; | ||
for (let i = 0; i < files.length; i++) { | ||
const activeFile = files[i]; | ||
const metadata = this.plugin.app.metadataCache.getCache(activeFile.path); | ||
if (!metadata || !metadata.frontmatter) { | ||
return; | ||
} | ||
if (metadata.frontmatter["relay-document"] == document_id) | ||
located_files.push(activeFile); | ||
} | ||
return located_files; | ||
} | ||
|
||
async send_document(activeFile: TFile | null) { | ||
if (!activeFile) { | ||
return; | ||
} | ||
// File not markdown | ||
if (activeFile.extension !== "md") { | ||
//NOTE: This gets called on all files on the vault potentially (e.g. syncing) | ||
//So, we don't call the Notice here, might want to do if the method is called manually though. | ||
//new Notice( | ||
// "The current file is not a markdown file. Please open a markdown file and try again.", | ||
//); | ||
return; | ||
} | ||
const metadata = this.plugin.app.metadataCache.getCache(activeFile.path); | ||
|
||
// There is no metadata so it cannot possibly be shared with anyone | ||
if (!metadata || !metadata.frontmatter) { | ||
return; | ||
} | ||
|
||
// We only share if relay-to is provided, even if its empty | ||
if (!("relay-to" in metadata.frontmatter) | ||
|| !(metadata.frontmatter["relay-to"])) { | ||
return | ||
} | ||
|
||
// Do we already have an id maybe? | ||
let id = metadata.frontmatter["relay-document"] | ||
|
||
// Get the content of the file | ||
const body = await this.plugin.app.vault.cachedRead(activeFile); | ||
|
||
// Either we POST a new document or we PUT an existing document | ||
let response; | ||
if (id) { | ||
// TODO: In this case, we also need to check if we maybe have to update the assets we originally had in the document as well | ||
// this also means that we may have to delete embeds that have been removed from the note since it has been last | ||
// sent to the API | ||
response = await this.api.put('/v1/doc/' + id, body); | ||
} else { | ||
response = await this.api.post('/v1/doc?filename=' + encodeURIComponent(activeFile.path), body); | ||
} | ||
if (!response) return; | ||
|
||
console.log("Successfully sent to " + this.plugin.settings.base_uri); | ||
|
||
try { | ||
// WARNING: This overwrites an existing relay-document id potentially, | ||
// depending on how the server responds. It's a feature, and not a bug and | ||
// allows the backend to decide if a new document should be | ||
// created, or the old one should be updated, depending on | ||
// permissions. | ||
id = response.json.result["relay-document"]; | ||
// update document to contain new document id | ||
this.plugin.app.fileManager.processFrontMatter(activeFile, (frontmatter: any) => { | ||
frontmatter["relay-document"] = id; | ||
}); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
|
||
// Now try upload the embeds | ||
if (!metadata.embeds) { | ||
return; | ||
} | ||
metadata.embeds.map(async (item) => { | ||
let file = this.plugin.app.vault.getAbstractFileByPath(item.link); | ||
if (!(file instanceof TFile)) { | ||
file = this.plugin.app.metadataCache.getFirstLinkpathDest(item.link, ""); | ||
} | ||
if (!file || !(file instanceof TFile)) { | ||
console.log(`Embed ${item.link} was not found!`) | ||
} else { | ||
this.embed_repo.upload_asset(id, item.link, file); | ||
} | ||
}); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { Notice, TFile, TFolder, normalizePath } from "obsidian" | ||
import RelayMdPLugin from './main' | ||
import { API } from "./api" | ||
import { Utils } from "./utils" | ||
import { IDocument } from './document'; | ||
|
||
export interface IEmbed { | ||
checksum_sha256: string; | ||
filename: string; | ||
filesize: number; | ||
id: string; | ||
} | ||
|
||
|
||
export class EmbedRepo { | ||
plugin: RelayMdPLugin | ||
api: API | ||
|
||
constructor(plugin: RelayMdPLugin) { | ||
this.plugin = plugin | ||
this.api = new API(this.plugin); | ||
} | ||
|
||
async load_embeds(embed: IEmbed, document: IDocument) { | ||
// TODO: think about this for a bit, it allows to update other peoples file by just using the same filename | ||
// On the other hand, if we were to put the team name into the path, we end up (potentially) having tos | ||
// duplicate the file into multiple team's attachment folder. Hmm | ||
document["relay-to"].forEach(async (team_topic: string) => { | ||
const parts = team_topic.split("@", 2); | ||
const team = parts[1]; | ||
if (!team) return; | ||
const folder = normalizePath(this.plugin.settings.vault_base_folder + "/" + team + "/_attachments"); | ||
const path = normalizePath(folder + "/" + embed.filename); | ||
Utils.make_directory_recursively(this.plugin.app, path); | ||
const file = this.plugin.app.vault.getAbstractFileByPath(path); | ||
if (file instanceof TFile) { | ||
const local_content = await this.plugin.app.vault.readBinary(file); | ||
const checksum = await Utils.calculateSHA256Checksum(local_content); | ||
if (checksum != embed.checksum_sha256) { | ||
const content = await this.get_embeded_binary(embed); | ||
if (!content) return; | ||
this.plugin.app.vault.modifyBinary(file, content); | ||
console.log("Binary file " + path + " has been updated!"); | ||
} | ||
} else { | ||
const content = await this.get_embeded_binary(embed); | ||
if (!content) return; | ||
this.plugin.app.vault.createBinary(path, content); | ||
console.log("Binary file " + path + " has been created!"); | ||
} | ||
}) | ||
} | ||
|
||
async get_embeded_binary(embed: IEmbed) { | ||
const response = await this.api.get('/v1/assets/' + embed.id, 'application/octet-stream') | ||
if (!response) return; | ||
return response.arrayBuffer; | ||
} | ||
|
||
|
||
async upload_asset(id: string, link: string, file: TFile) { | ||
const content = await this.plugin.app.vault.readBinary(file); | ||
const response = await this.api.postRaw('/v1/assets/' + id + '?filename=' + encodeURIComponent(file.path), content) | ||
if (!response) return; | ||
console.log("Successfully uploaded " + file.path + " as " + response.json.result.id); | ||
} | ||
} |
Oops, something went wrong.