From e55f2a6b11e76e5344c8ac98761de0bf35d1ea09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Zbytovsk=C3=BD?= Date: Thu, 23 Nov 2023 08:12:23 +0100 Subject: [PATCH 01/45] upload - add upload button --- pages/api/upload.ts | 32 +++++++++ src/components/FeaturePanel/FeaturePanel.tsx | 2 + .../UploadDialog/UploadDialog.tsx | 69 +++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 pages/api/upload.ts create mode 100644 src/components/FeaturePanel/UploadDialog/UploadDialog.tsx diff --git a/pages/api/upload.ts b/pages/api/upload.ts new file mode 100644 index 000000000..378a2bb45 --- /dev/null +++ b/pages/api/upload.ts @@ -0,0 +1,32 @@ +import { writeFile } from 'fs/promises' +import { NextRequest, NextResponse } from 'next/server' +import type { NextApiRequest, NextApiResponse } from 'next' + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === 'POST') { + POST(req) + } else { + // Handle any other HTTP method + } +} + + +export async function POST(request: NextRequest) { + const data = await request.formData() + const file: File | null = data.get('file') as unknown as File + + if (!file) { + return NextResponse.json({ success: false }) + } + + const bytes = await file.arrayBuffer() + const buffer = Buffer.from(bytes) + + // With the file data in the buffer, you can do whatever you want with it. + // For this, we'll just write it to the filesystem in a new location + const path = `/tmp/${file.name}` + await writeFile(path, buffer) + console.log(`open ${path} to see the uploaded file`) + + return NextResponse.json({ success: true }) +} diff --git a/src/components/FeaturePanel/FeaturePanel.tsx b/src/components/FeaturePanel/FeaturePanel.tsx index ad306ec1c..90dbb2faa 100644 --- a/src/components/FeaturePanel/FeaturePanel.tsx +++ b/src/components/FeaturePanel/FeaturePanel.tsx @@ -22,6 +22,7 @@ import { RouteDistributionInPanel } from './Climbing/RouteDistribution'; import { RouteListInPanel } from './Climbing/RouteList/RouteList'; import { FeaturePanelFooter } from './FeaturePanelFooter'; import { ClimbingRouteGrade } from './ClimbingRouteGrade'; +import { UploadDialog } from './UploadDialog/UploadDialog'; const Flex = styled.div` flex: 1; @@ -61,6 +62,7 @@ export const FeaturePanel = () => { + diff --git a/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx b/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx new file mode 100644 index 000000000..ca0bcca84 --- /dev/null +++ b/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx @@ -0,0 +1,69 @@ +import React, { ChangeEvent, useState } from "react"; +import { fetchJson } from "../../../services/fetch"; +import { Button } from "@material-ui/core"; +import { useFeatureContext } from "../../utils/FeatureContext"; + +const UploadButton = () => { + const [uploading, setUploading] = useState(false); + + const handleFileUpload = async (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) { + return; + } + setUploading(true); + + const formData = new FormData(); + formData.append("filename", file.name); + formData.append("file", file); + + const uploadResponse = await fetchJson( + process.env.NEXT_PUBLIC_BASE_URL + "/api/upload", + { + method: "POST", + body: formData + } + ); + + console.log("uploadResponse", uploadResponse); + + setUploading(false); + }; + + return ( + <> + + + ); +}; + +export const UploadDialog = () => { + const { feature } = useFeatureContext(); + const { osmMeta, skeleton } = feature; + const editEnabled = !skeleton; + + return ( + <> + {editEnabled && ( + <> +
+ + + )} + + ); +}; From 6b78b3493639adbd78a46f9cffd348688185e23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Zbytovsk=C3=BD?= Date: Thu, 30 Nov 2023 21:29:01 +0100 Subject: [PATCH 02/45] upload - parse uploads correctly, import wikiapi --- package.json | 2 + pages/api/upload.ts | 148 +++++++++++++++--- .../UploadDialog/UploadDialog.tsx | 37 +++-- src/services/fetch.ts | 3 +- src/services/fetchCache.ts | 16 +- 5 files changed, 165 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 3ad1ebce1..d3868a6ad 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "date-fns": "^3.6.0", "image-size": "^1.1.1", "isomorphic-unfetch": "^4.0.2", + "formidable": "^3.5.1", "isomorphic-xml2js": "^0.1.3", "js-cookie": "^3.0.5", "js-md5": "^0.8.3", @@ -60,6 +61,7 @@ }, "devDependencies": { "@types/autosuggest-highlight": "^3.2.3", + "@types/formidable": "^3.4.5", "@types/jest": "^29.5.12", "@types/react-dom": "^18.3.0", "@typescript-eslint/typescript-estree": "^8.2.0", diff --git a/pages/api/upload.ts b/pages/api/upload.ts index 378a2bb45..aa8d48cdf 100644 --- a/pages/api/upload.ts +++ b/pages/api/upload.ts @@ -1,32 +1,128 @@ -import { writeFile } from 'fs/promises' -import { NextRequest, NextResponse } from 'next/server' -import type { NextApiRequest, NextApiResponse } from 'next' - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === 'POST') { - POST(req) - } else { - // Handle any other HTTP method - } -} +import type { NextApiRequest, NextApiResponse } from 'next'; +import formidable from 'formidable'; +import Wikiapi from 'wikiapi'; +export const config = { + api: { + bodyParser: false, + }, +}; -export async function POST(request: NextRequest) { - const data = await request.formData() - const file: File | null = data.get('file') as unknown as File +/* +alg: + +sanitize title +check duplicate by sha1 +construct description (categories) +each file must belong to at least one category that describes its content or function +get category based on location eg +check for API errors +send structured data + +reply 200 + { name, url} to show Succes Dialog + +pokud se vkládal nový osm prvek, tak updatnout link a přidat rename do description: +{{Rename|required newname.ext|required rationale number|reason=required text reason}} + + + */ + +// https://commons.wikimedia.org/wiki/File:Drive_approaching_the_Grecian_Lodges_-_geograph.org.uk_-_5765640.jpg +// https://github.com/multichill/toollabs/blob/master/bot/commons/geograph_uploader.py + +const uploadToWikimediaCommons = async ( + filepath: string, + filename: string, + osmEntity: string, + username: string = 'zby-cz', + userId: string = '123', +) => { + const filename = 'aktuální nazev z dialogu nebo souřadnice - OsmAPP.org - node/123.jpg'; + + const featureName = 'nazev z dialogu nebo souřadnice'; + const featureLocation = 'z dialogu'; - if (!file) { - return NextResponse.json({ success: false }) - } - const bytes = await file.arrayBuffer() - const buffer = Buffer.from(bytes) + // EXIF location ... - // With the file data in the buffer, you can do whatever you want with it. - // For this, we'll just write it to the filesystem in a new location - const path = `/tmp/${file.name}` - await writeFile(path, buffer) - console.log(`open ${path} to see the uploaded file`) - return NextResponse.json({ success: true }) -} + const wiki = new Wikiapi(); + await wiki.login('OsmappBot', 'password', 'test'); + + /* ***************************************************** */ + /* FILES *********************************************** */ + // Note: parameter `text`, filled with the right wikicode `{{description|}}`, can replace most parameters. + let options = { + description: 'Photo of Osaka', + date: new Date(), + source_url: 'https://github.com/kanasimi/wikiapi', + author: `[https://www.openstreetmap.org/user/${username} ${username}] (${userId})`, + permission: '{{cc-by-sa-2.5}}', + other_versions: '', + other_fields: '', + license: ['{{cc-by-sa-2.5}}'], + categories: ['[[Category:test images]]'], + bot: 1, + tags: 'tag1|tag2', + }; + + let result = await wiki.upload({ + file_path: filepath, + filename: filename, + comment: '', + ignorewarnings: 1, // overwrite existing file + ...options, + }); +}; + +// TODO upgrade Nextjs and use export async function POST(request: NextRequest) { +export default async (req: NextApiRequest, res: NextApiResponse) => { + try { + const form = formidable({ uploadDir: '/tmp' }); + const [fields, files] = await form.parse(req); + + const path = files.file[0].filepath; + const size = files.file[0].size; + const name = fields.filename[0]; + const osmEntity = fields.osmEntity[0]; + + if (size > 100 * 1024 * 1024) { + throw new Error('File larger than 100MB'); + } + + await uploadToWikimediaCommons(path, name, osmEntity); + + res.status(200).json({ + status: 'ok', + path, + name, + osmEntity, + }); + } catch (err) { + console.error(err); + res.status(err.httpCode || 400).send(String(err)); + } +}; + +// import { writeFile } from 'fs/promises' +// import { NextRequest, NextResponse } from 'next/server' +// +// export async function POST(request: NextRequest) { +// const data = await request.formData() +// const file: File | null = data.get('file') as unknown as File +// +// if (!file) { +// return NextResponse.json({ success: false }) +// } +// +// const bytes = await file.arrayBuffer() +// const buffer = Buffer.from(bytes) +// +// // With the file data in the buffer, you can do whatever you want with it. +// // For this, we'll just write it to the filesystem in a new location +// const path = `/tmp/${file.name}` +// await writeFile(path, buffer) +// console.log(`open ${path} to see the uploaded file`) +// +// return NextResponse.json({ success: true }) +// } diff --git a/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx b/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx index ca0bcca84..7ada82d6e 100644 --- a/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx +++ b/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx @@ -1,33 +1,46 @@ import React, { ChangeEvent, useState } from "react"; -import { fetchJson } from "../../../services/fetch"; +import { fetchText } from "../../../services/fetch"; import { Button } from "@material-ui/core"; import { useFeatureContext } from "../../utils/FeatureContext"; +import { getUrlOsmId } from "../../../services/helpers"; const UploadButton = () => { + const { feature } = useFeatureContext(); const [uploading, setUploading] = useState(false); const handleFileUpload = async (e: ChangeEvent) => { const file = e.target.files?.[0]; + if (!file) { return; } + + if (file.size > 100 * 1024 * 1024) { + alert("Maximum file size is 100 MB."); + return; + } + setUploading(true); const formData = new FormData(); formData.append("filename", file.name); formData.append("file", file); + formData.append("osmEntity", getUrlOsmId(feature.osmMeta)); - const uploadResponse = await fetchJson( - process.env.NEXT_PUBLIC_BASE_URL + "/api/upload", - { - method: "POST", - body: formData - } - ); - - console.log("uploadResponse", uploadResponse); + try { + const uploadResponse = await fetchText( + "/api/upload", + { + method: "POST", + body: formData + } + ); - setUploading(false); + console.log("uploadResponse", uploadResponse); + } + finally { + setUploading(false); + } }; return ( @@ -42,7 +55,7 @@ const UploadButton = () => { Nahrát obrázek diff --git a/src/services/fetch.ts b/src/services/fetch.ts index de35dfaef..04c82db47 100644 --- a/src/services/fetch.ts +++ b/src/services/fetch.ts @@ -20,6 +20,7 @@ interface FetchOpts extends RequestInit { export const fetchText = async (url, opts: FetchOpts = {}) => { const key = getKey(url, opts); + console.log('fetchText', key, url, opts) const item = getCache(key); if (item) return item; @@ -49,7 +50,7 @@ export const fetchText = async (url, opts: FetchOpts = {}) => { } const text = await res.text(); - if (!opts || !opts.nocache) { + if (!opts || !opts.nocache || opts.method !== 'POST') { writeCacheSafe(key, text); } return text; diff --git a/src/services/fetchCache.ts b/src/services/fetchCache.ts index 5a4ac2812..d038024c7 100644 --- a/src/services/fetchCache.ts +++ b/src/services/fetchCache.ts @@ -18,15 +18,27 @@ const fetchCache = isBrowser() clear: () => {}, }; -export const getKey = (url, opts) => url + JSON.stringify(opts); +export const getKey = (url, opts) => { + if (opts.method === 'POST') { + return false; + } + + return url + JSON.stringify(opts); +}; -export const getCache = fetchCache.get; +export const getCache = (key) => { + if (key) { + return fetchCache.get(key); + } +}; export const removeFetchCache = (url, opts = {}) => { fetchCache.remove(getKey(url, opts)); }; export const writeCacheSafe = (key, value) => { + if (!key) return; + try { fetchCache.put(key, value); } catch (e) { From afacb76a85a44d592df2d0679d97fcae479ffae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Zbytovsk=C3=BD?= Date: Sat, 2 Dec 2023 15:09:20 +0100 Subject: [PATCH 03/45] upload - description --- pages/api/fixture.ts | 47 ++++++++++++ pages/api/upload.ts | 178 ++++++++++++++++++++++++++++--------------- 2 files changed, 164 insertions(+), 61 deletions(-) create mode 100644 pages/api/fixture.ts diff --git a/pages/api/fixture.ts b/pages/api/fixture.ts new file mode 100644 index 000000000..fa70a085b --- /dev/null +++ b/pages/api/fixture.ts @@ -0,0 +1,47 @@ + + +export const exampleUploadResponse = { + "upload": { + "filename": "File_1.jpg", + "result": "Success", + "imageinfo": { + "url": "https://upload.wikimedia.org/wikipedia/test/3/39/File_1.jpg", + "html": "

A file with this name exists already, please check File:File 1.jpg if you are not sure if you want to change it.\n

\n\n", + "width": 474, + "size": 26703, + "bitdepth": 8, + "mime": "image/jpeg", + "userid": 42588, + "mediatype": "BITMAP", + "descriptionurl": "https://test.wikipedia.org/wiki/File:File_1.jpg", + "extmetadata": { + "ObjectName": { + "value": "File 1", + "hidden": "", + "source": "mediawiki-metadata" + }, + "DateTime": { + "value": "2019-03-06 08:43:37", + "hidden": "", + "source": "mediawiki-metadata" + } + // ... + }, + "comment": "", + "commonmetadata": [], + "descriptionshorturl": "https://test.wikipedia.org/w/index.php?curid=0", + "sha1": "2ffadd0da73fab31a50407671622fd6e5282d0cf", + "parsedcomment": "", + "metadata": [ + { + "name": "MEDIAWIKI_EXIF_VERSION", + "value": 2 + } + ], + "canonicaltitle": "File:File 1.jpg", + "user": "Mansi29ag", + "timestamp": "2019-03-06T08:43:37Z", + "height": 296 + } + } +} diff --git a/pages/api/upload.ts b/pages/api/upload.ts index aa8d48cdf..ce146b229 100644 --- a/pages/api/upload.ts +++ b/pages/api/upload.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import formidable from 'formidable'; import Wikiapi from 'wikiapi'; +import fs from 'fs'; export const config = { api: { @@ -8,53 +9,99 @@ export const config = { }, }; -/* -alg: - -sanitize title -check duplicate by sha1 -construct description (categories) -each file must belong to at least one category that describes its content or function -get category based on location eg -check for API errors -send structured data - -reply 200 + { name, url} to show Succes Dialog - -pokud se vkládal nový osm prvek, tak updatnout link a přidat rename do description: -{{Rename|required newname.ext|required rationale number|reason=required text reason}} - - - */ - // https://commons.wikimedia.org/wiki/File:Drive_approaching_the_Grecian_Lodges_-_geograph.org.uk_-_5765640.jpg // https://github.com/multichill/toollabs/blob/master/bot/commons/geograph_uploader.py +// MD5 hash wikidata https://commons.wikimedia.org/w/index.php?title=File%3AArea_needs_fixing-Syria_map.png&diff=801153548&oldid=607140167 +// https://commons.wikimedia.org/wiki/Template:Geograph_from_structured_data + +const uploadToWikimediaCommons = async (filepath: string) => { + const username = 'zby-cz'; + const userId = '123123'; + const name = 'Na Vrškách'; + const location = [50.123, 14.123]; + const presetKey = 'amenity/restaurant'; + const presetName = 'Restaurace'; + const filename = 'IMG_1234.jpg'; + const osmEntity = 'node/11111111'; + const filemtime = fs.statSync(filepath).mtime.toISOString(); //.replace(/\.\d+Z$/, 'Z'); // případně file[0].mtime + // TODO EXIF location, date information.date = {{According to Exif data|2023-11-16}} + + // transform + const extension = filename.split('.').pop(); + const title = `${presetName} ${name || location} - OsmAPP.${extension}`; // Restaurace Na Vrškách - OsmAPP.jpg + + // TODO construct description (categories) + // TODO each file must belong to at least one category that describes its content or function + // TODO get category based on location eg + + + // checks + + const text = `{{description|1=${presetKey}|2=${name}|3=${location}}}`; // {{description|amenity/restaurant|Na Vrškách|50.123,14.123}} + + + // camera location + // location made + + // language=html + const text2 = ` +

{{int:filedesc}}

+ {{Information + |description = ${presetName} ${name || location} + |date = ${filemtime} + |source = {{Own photo}} + |author = OpenStreetMap user [https://www.openstreetmap.org/user/${username}#id${userId} ${username}] + |other_fields_1 = + {{Information field + |name = {{Label|P180|link=-|capitalization=ucfirst}} + |value = {{#property:P180|from=M{{PAGEID}} }} [[File:OOjs UI icon edit-ltr-progressive.svg |frameless |text-top |10px |link={{fullurl:{{FULLPAGENAME}}}}#P180|alt=Edit this on Structured Data on Commons|Edit this on Structured Data on Commons]] + }} + |other_fields = + {{Information field + |name = {{ucfirst: {{I18n/location|made}} }} + |value = {{#invoke:Information|SDC_Location|icon=true}} {{#if:{{#property:P1071|from=M{{PAGEID}} }}|({{#invoke:PropertyChain|PropertyChain|qID={{#invoke:WikidataIB |followQid |props=P1071}}|pID=P131|endpID=P17}})}} + }} + }} + +

{{int:license-header}}

+ {{Self|cc-by-4.0|author=OpenStreetMap user [https://www.openstreetmap.org/user/${username}#id${userId} ${username}]}} + {{FoP-Czech_Republic}} + `; + // TODO choose correct FOP based on country: https://commons.wikimedia.org/wiki/Category:FoP_templates -const uploadToWikimediaCommons = async ( - filepath: string, - filename: string, - osmEntity: string, - username: string = 'zby-cz', - userId: string = '123', -) => { - const filename = 'aktuální nazev z dialogu nebo souřadnice - OsmAPP.org - node/123.jpg'; - const featureName = 'nazev z dialogu nebo souřadnice'; - const featureLocation = 'z dialogu'; + const wiki = new Wikiapi(); + await wiki.login('OsmappBot', 'password', 'test'); - // EXIF location ... + // TODO check duplicate by sha1 + // SEND - const wiki = new Wikiapi(); - await wiki.login('OsmappBot', 'password', 'test'); + /* https://www.mediawiki.org/wiki/API:Upload + filename //Target filename. + comment //Upload comment. Also used as the initial page text for new files if text is not specified. + tags //Change tags to apply to the upload log entry and file page revision. + text //Initial page text for new files. + ignorewarnings //Ignore any warnings. + file // Must be posted as a file upload using multipart/form-data. + filekey // Key that identifies a previous upload that was stashed temporarily. + stash // If set, the server will stash the file temporarily instead of adding it to the repository. + filesize + // offset // Offset of chunk in bytes. + // chunk Must be posted as a file upload using multipart/form-data. -- chunks 1 MB + // async // Make potentially large file operations asynchronous when possible. + checkstatus // Only fetch the upload status for the given file key. + token // A "csrf" token retrieved from action=query&meta=tokens + * */ - /* ***************************************************** */ - /* FILES *********************************************** */ - // Note: parameter `text`, filled with the right wikicode `{{description|}}`, can replace most parameters. - let options = { - description: 'Photo of Osaka', - date: new Date(), + let result = await wiki.upload({ + file_path: filepath, + filename: title, + comment: 'Initial upload from OsmAPP.org', + ignorewarnings: 1, // overwrite existing file + description: text, + date: filemtime, source_url: 'https://github.com/kanasimi/wikiapi', author: `[https://www.openstreetmap.org/user/${username} ${username}] (${userId})`, permission: '{{cc-by-sa-2.5}}', @@ -64,39 +111,48 @@ const uploadToWikimediaCommons = async ( categories: ['[[Category:test images]]'], bot: 1, tags: 'tag1|tag2', - }; - let result = await wiki.upload({ - file_path: filepath, - filename: filename, - comment: '', - ignorewarnings: 1, // overwrite existing file - ...options, + token: '', // TODO ??? GET a CSRF token: api.php?action=query&format=json&meta=tokens }); + + // TODO ošetřit existující filename jakoby (2) + // TODO check for API errors + + // TODO send structured data + + + // Step 3: Final upload using the filekey to commit the upload out of the stash area + // api.php?action=upload&format=json&filename=File_1.jpg&filekey=somefilekey1234.jpg&token=123Token&comment=upload_comment_here&text=file_description [try in ApiSandbox] + + + // TODO LATER pokud se vkládal nový osm prvek, tak updatnout link a přidat rename do description: + // {{Rename|required newname.ext|required rationale number|reason=required text reason}} }; // TODO upgrade Nextjs and use export async function POST(request: NextRequest) { export default async (req: NextApiRequest, res: NextApiResponse) => { try { - const form = formidable({ uploadDir: '/tmp' }); - const [fields, files] = await form.parse(req); - - const path = files.file[0].filepath; - const size = files.file[0].size; - const name = fields.filename[0]; - const osmEntity = fields.osmEntity[0]; - - if (size > 100 * 1024 * 1024) { - throw new Error('File larger than 100MB'); - } - - await uploadToWikimediaCommons(path, name, osmEntity); + // const form = formidable({ uploadDir: '/tmp' }); + // const [fields, files] = await form.parse(req); + // + // const path = files.file[0].filepath; + // const size = files.file[0].size; + // const filemtime = files.file[0].mtime; + // const name = fields.filename[0]; + // const osmEntity = fields.osmEntity[0]; + // + // if (size > 100 * 1024 * 1024) { + // throw new Error('File larger than 100MB'); + // } + + await uploadToWikimediaCommons('./IMG_3379.HEIC'); + // reply 200 + { name, url} to show Succes Dialog res.status(200).json({ status: 'ok', - path, - name, - osmEntity, + // path, + // name, + // osmEntity, }); } catch (err) { console.error(err); From d7c80d3189b4821d67994de3db7924b1d31cba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Zbytovsk=C3=BD?= Date: Thu, 18 Apr 2024 11:45:11 +0200 Subject: [PATCH 04/45] upload file + fetch user --- pages/api/fixture.ts | 80 ++++++++-------- pages/api/upload.ts | 96 ++++++++----------- .../UploadDialog/UploadDialog.tsx | 88 +++++++++++------ src/services/fetch.ts | 2 +- src/services/helpers.ts | 2 +- src/services/osmApiAuth.ts | 2 - src/services/osmApiAuthServer.ts | 6 +- 7 files changed, 143 insertions(+), 133 deletions(-) diff --git a/pages/api/fixture.ts b/pages/api/fixture.ts index fa70a085b..25e458f04 100644 --- a/pages/api/fixture.ts +++ b/pages/api/fixture.ts @@ -1,47 +1,45 @@ - - export const exampleUploadResponse = { - "upload": { - "filename": "File_1.jpg", - "result": "Success", - "imageinfo": { - "url": "https://upload.wikimedia.org/wikipedia/test/3/39/File_1.jpg", - "html": "

A file with this name exists already, please check File:File 1.jpg if you are not sure if you want to change it.\n

\n\n", - "width": 474, - "size": 26703, - "bitdepth": 8, - "mime": "image/jpeg", - "userid": 42588, - "mediatype": "BITMAP", - "descriptionurl": "https://test.wikipedia.org/wiki/File:File_1.jpg", - "extmetadata": { - "ObjectName": { - "value": "File 1", - "hidden": "", - "source": "mediawiki-metadata" + upload: { + filename: 'File_1.jpg', + result: 'Success', + imageinfo: { + url: 'https://upload.wikimedia.org/wikipedia/test/3/39/File_1.jpg', + html: '

A file with this name exists already, please check File:File 1.jpg if you are not sure if you want to change it.\n

\n\n', + width: 474, + size: 26703, + bitdepth: 8, + mime: 'image/jpeg', + userid: 42588, + mediatype: 'BITMAP', + descriptionurl: 'https://test.wikipedia.org/wiki/File:File_1.jpg', + extmetadata: { + ObjectName: { + value: 'File 1', + hidden: '', + source: 'mediawiki-metadata', + }, + DateTime: { + value: '2019-03-06 08:43:37', + hidden: '', + source: 'mediawiki-metadata', }, - "DateTime": { - "value": "2019-03-06 08:43:37", - "hidden": "", - "source": "mediawiki-metadata" - } // ... }, - "comment": "", - "commonmetadata": [], - "descriptionshorturl": "https://test.wikipedia.org/w/index.php?curid=0", - "sha1": "2ffadd0da73fab31a50407671622fd6e5282d0cf", - "parsedcomment": "", - "metadata": [ + comment: '', + commonmetadata: [], + descriptionshorturl: 'https://test.wikipedia.org/w/index.php?curid=0', + sha1: '2ffadd0da73fab31a50407671622fd6e5282d0cf', + parsedcomment: '', + metadata: [ { - "name": "MEDIAWIKI_EXIF_VERSION", - "value": 2 - } + name: 'MEDIAWIKI_EXIF_VERSION', + value: 2, + }, ], - "canonicaltitle": "File:File 1.jpg", - "user": "Mansi29ag", - "timestamp": "2019-03-06T08:43:37Z", - "height": 296 - } - } -} + canonicaltitle: 'File:File 1.jpg', + user: 'Mansi29ag', + timestamp: '2019-03-06T08:43:37Z', + height: 296, + }, + }, +}; diff --git a/pages/api/upload.ts b/pages/api/upload.ts index ce146b229..f5f74c9ee 100644 --- a/pages/api/upload.ts +++ b/pages/api/upload.ts @@ -2,6 +2,11 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import formidable from 'formidable'; import Wikiapi from 'wikiapi'; import fs from 'fs'; +import { + serverFetchOsmUser, + ServerOsmUser, +} from '../../src/services/osmApiAuthServer'; +import { fetchFeature } from '../../src/services/osmApi'; export const config = { api: { @@ -14,16 +19,19 @@ export const config = { // MD5 hash wikidata https://commons.wikimedia.org/w/index.php?title=File%3AArea_needs_fixing-Syria_map.png&diff=801153548&oldid=607140167 // https://commons.wikimedia.org/wiki/Template:Geograph_from_structured_data -const uploadToWikimediaCommons = async (filepath: string) => { - const username = 'zby-cz'; - const userId = '123123'; +const uploadToWikimediaCommons = async ( + user: ServerOsmUser, + filepath: string, +) => { + const {username} = user; + const userId = user.id; const name = 'Na Vrškách'; const location = [50.123, 14.123]; const presetKey = 'amenity/restaurant'; const presetName = 'Restaurace'; const filename = 'IMG_1234.jpg'; const osmEntity = 'node/11111111'; - const filemtime = fs.statSync(filepath).mtime.toISOString(); //.replace(/\.\d+Z$/, 'Z'); // případně file[0].mtime + const filemtime = fs.statSync(filepath).mtime.toISOString(); // .replace(/\.\d+Z$/, 'Z'); // případně file[0].mtime // TODO EXIF location, date information.date = {{According to Exif data|2023-11-16}} // transform @@ -34,12 +42,10 @@ const uploadToWikimediaCommons = async (filepath: string) => { // TODO each file must belong to at least one category that describes its content or function // TODO get category based on location eg - // checks const text = `{{description|1=${presetKey}|2=${name}|3=${location}}}`; // {{description|amenity/restaurant|Na Vrškách|50.123,14.123}} - // camera location // location made @@ -69,11 +75,9 @@ const uploadToWikimediaCommons = async (filepath: string) => { `; // TODO choose correct FOP based on country: https://commons.wikimedia.org/wiki/Category:FoP_templates - const wiki = new Wikiapi(); await wiki.login('OsmappBot', 'password', 'test'); - // TODO check duplicate by sha1 // SEND @@ -95,7 +99,7 @@ const uploadToWikimediaCommons = async (filepath: string) => { token // A "csrf" token retrieved from action=query&meta=tokens * */ - let result = await wiki.upload({ + const result = await wiki.upload({ file_path: filepath, filename: title, comment: 'Initial upload from OsmAPP.org', @@ -120,11 +124,9 @@ const uploadToWikimediaCommons = async (filepath: string) => { // TODO send structured data - // Step 3: Final upload using the filekey to commit the upload out of the stash area // api.php?action=upload&format=json&filename=File_1.jpg&filekey=somefilekey1234.jpg&token=123Token&comment=upload_comment_here&text=file_description [try in ApiSandbox] - // TODO LATER pokud se vkládal nový osm prvek, tak updatnout link a přidat rename do description: // {{Rename|required newname.ext|required rationale number|reason=required text reason}} }; @@ -132,53 +134,37 @@ const uploadToWikimediaCommons = async (filepath: string) => { // TODO upgrade Nextjs and use export async function POST(request: NextRequest) { export default async (req: NextApiRequest, res: NextApiResponse) => { try { - // const form = formidable({ uploadDir: '/tmp' }); - // const [fields, files] = await form.parse(req); - // - // const path = files.file[0].filepath; - // const size = files.file[0].size; - // const filemtime = files.file[0].mtime; - // const name = fields.filename[0]; - // const osmEntity = fields.osmEntity[0]; - // - // if (size > 100 * 1024 * 1024) { - // throw new Error('File larger than 100MB'); - // } - - await uploadToWikimediaCommons('./IMG_3379.HEIC'); - // reply 200 + { name, url} to show Succes Dialog + const form = formidable({ uploadDir: '/tmp' }); + const [fields, files] = await form.parse(req); + + const path = files.file[0].filepath; + const {size} = files.file[0]; + const filemtime = files.file[0].mtime; + + const name = fields.filename[0]; + const osmShortId = fields.osmShortId[0]; + if (size > 100 * 1024 * 1024) { + throw new Error('File larger than 100MB'); + } + + const { osmAccessToken } = req.cookies; + const user = await serverFetchOsmUser({ osmAccessToken }); + const feature = await fetchFeature(osmShortId); + + // await uploadToWikimediaCommons(user, './IMG_3379.HEIC'); res.status(200).json({ - status: 'ok', - // path, - // name, - // osmEntity, + user, + path, + size, + filemtime, + name, + osmShortId, + feature, + success: true, }); } catch (err) { - console.error(err); - res.status(err.httpCode || 400).send(String(err)); + console.error(err); // eslint-disable-line no-console + res.status(err.code ?? 400).send(String(err)); } }; - -// import { writeFile } from 'fs/promises' -// import { NextRequest, NextResponse } from 'next/server' -// -// export async function POST(request: NextRequest) { -// const data = await request.formData() -// const file: File | null = data.get('file') as unknown as File -// -// if (!file) { -// return NextResponse.json({ success: false }) -// } -// -// const bytes = await file.arrayBuffer() -// const buffer = Buffer.from(bytes) -// -// // With the file data in the buffer, you can do whatever you want with it. -// // For this, we'll just write it to the filesystem in a new location -// const path = `/tmp/${file.name}` -// await writeFile(path, buffer) -// console.log(`open ${path} to see the uploaded file`) -// -// return NextResponse.json({ success: true }) -// } diff --git a/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx b/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx index 7ada82d6e..792ff296a 100644 --- a/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx +++ b/src/components/FeaturePanel/UploadDialog/UploadDialog.tsx @@ -1,56 +1,82 @@ -import React, { ChangeEvent, useState } from "react"; -import { fetchText } from "../../../services/fetch"; -import { Button } from "@material-ui/core"; -import { useFeatureContext } from "../../utils/FeatureContext"; -import { getUrlOsmId } from "../../../services/helpers"; +import React, { ChangeEvent, useState } from 'react'; +import { Button } from '@material-ui/core'; +import { fetchText } from '../../../services/fetch'; +import { useFeatureContext } from '../../utils/FeatureContext'; +import { getShortId } from '../../../services/helpers'; +import { loginAndfetchOsmUser } from '../../../services/osmApiAuth'; -const UploadButton = () => { - const { feature } = useFeatureContext(); - const [uploading, setUploading] = useState(false); +const WIKIPEDIA_LIMIT = 100 * 1024 * 1024; + +const performUpload = async (file, feature) => { + const formData = new FormData(); + formData.append('filename', file.name); + formData.append('file', file); + formData.append('osmShortId', getShortId(feature.osmMeta)); + + const uploadResponse = await fetchText('/api/upload', { + method: 'POST', + body: formData, + }); + + console.log('uploadResponse', uploadResponse); +}; + +const performUploadWithLogin = async (file, feature) => { + try { + await performUpload(file, feature); + } catch (e) { + if (e.code === '401') { + await loginAndfetchOsmUser(); + await performUpload(file, feature); + } + } +}; - const handleFileUpload = async (e: ChangeEvent) => { +const getHandleFileUpload = + (feature, setUploading, setResetKey) => + async (e: ChangeEvent) => { const file = e.target.files?.[0]; if (!file) { return; } - if (file.size > 100 * 1024 * 1024) { - alert("Maximum file size is 100 MB."); + if (file.size > WIKIPEDIA_LIMIT) { + alert('Maximum file size is 100 MB.'); // eslint-disable-line no-alert return; } - setUploading(true); - - const formData = new FormData(); - formData.append("filename", file.name); - formData.append("file", file); - formData.append("osmEntity", getUrlOsmId(feature.osmMeta)); - try { - const uploadResponse = await fetchText( - "/api/upload", - { - method: "POST", - body: formData - } - ); - - console.log("uploadResponse", uploadResponse); - } - finally { + setUploading(true); + await performUploadWithLogin(file, feature); + } finally { setUploading(false); + setResetKey((key) => key + 1); } }; +const UploadButton = () => { + const { feature } = useFeatureContext(); + const [uploading, setUploading] = useState(false); + const [resetKey, setResetKey] = useState(0); + + const handleFileUpload = getHandleFileUpload( + feature, + setUploading, + setResetKey, + ); + return ( <>