1
1
import { StationStats , type TableDataItem } from "../types.d.ts" ;
2
2
import { getWSData } from "./wind2speed.ts" ;
3
- import { Application , Router } from "@oak/oak" ;
3
+ import { Application , Context , Router } from "@oak/oak" ;
4
4
import { oakCors } from "https://deno.land/x/[email protected] /mod.ts" ;
5
5
import { stringify } from "jsr:@std/csv/stringify" ;
6
6
@@ -41,6 +41,7 @@ async function downloadWindData(stationId: number) {
41
41
"station" ,
42
42
data . station . id ,
43
43
] ) ;
44
+ const months = stationStats . value ?. months || [ ] ;
44
45
const latestEntryTimestamp = stationStats . value ?. latestEntryTimestamp || 0 ;
45
46
46
47
const newTableData = data . tableData . filter (
@@ -53,18 +54,28 @@ async function downloadWindData(stationId: number) {
53
54
const transaction = kv . atomic ( ) ;
54
55
55
56
for ( const entry of newTableData ) {
57
+ const date = parseObsTimeLocal ( entry . obsTimeLocal ) ;
58
+ const year = date . getFullYear ( ) ;
59
+ const month = date . getMonth ( ) ;
60
+
61
+ if ( ! months . some ( ( m ) => m . year === year && m . month === month ) ) {
62
+ months . push ( { year, month } ) ;
63
+ }
64
+
56
65
transaction . set (
57
- [ "windHistoryData" , data . station . id , entry . id ] ,
66
+ [ "windHistoryData" , data . station . id , year , month , entry . id ] ,
58
67
entry
59
68
) ;
60
69
}
61
70
transaction . set ( [ "station" , data . station . id ] , {
62
71
...data . station ,
63
- entries : ( stationStats . value ?. entries ?? 0 ) + newTableData . length ,
72
+ totalEntries :
73
+ ( stationStats . value ?. totalEntries ?? 0 ) + newTableData . length ,
64
74
latestEntryTimestamp : parseObsTimeLocal (
65
75
newTableData [ 0 ] . obsTimeLocal
66
76
) . getTime ( ) ,
67
- } ) ;
77
+ months,
78
+ } as StationStats ) ;
68
79
transaction . set ( [ "latestUpdatedStation" ] , stationId ) ;
69
80
70
81
const result = await transaction . commit ( ) ;
@@ -86,6 +97,20 @@ function parseObsTimeLocal(obsTimeLocal: string): Date {
86
97
87
98
const router = new Router ( ) ;
88
99
100
+ const authMiddleware = async ( ctx : Context , next : ( ) => Promise < unknown > ) => {
101
+ if (
102
+ ( ctx . request . headers . get ( "Authorization" ) ?? "" ) !==
103
+ ( Deno . env . get ( "PASSWORD" ) ?? "" )
104
+ ) {
105
+ ctx . response . status = 401 ;
106
+ ctx . response . body = {
107
+ msg : "Unauthorized" ,
108
+ } ;
109
+ return ;
110
+ }
111
+ await next ( ) ;
112
+ } ;
113
+
89
114
router . post ( "/login" , async ( ctx ) => {
90
115
const body = await ctx . request . body . text ( ) ;
91
116
const formData = new URLSearchParams ( body ) ;
@@ -104,18 +129,7 @@ router.get("/tracked-stations", async (ctx) => {
104
129
ctx . response . body = trackedStations ?? [ ] ;
105
130
} ) ;
106
131
107
- router . post ( "/tracked-stations" , async ( ctx ) => {
108
- if (
109
- ( ctx . request . headers . get ( "Authorization" ) ?? "" ) !==
110
- ( Deno . env . get ( "PASSWORD" ) ?? "" )
111
- ) {
112
- ctx . response . status = 401 ;
113
- ctx . response . body = {
114
- msg : "Unauthorized" ,
115
- } ;
116
- return ;
117
- }
118
-
132
+ router . post ( "/tracked-stations" , authMiddleware , async ( ctx ) => {
119
133
try {
120
134
const body = await ctx . request . body . text ( ) ;
121
135
const formData = new URLSearchParams ( body ) ;
@@ -140,6 +154,35 @@ router.post("/tracked-stations", async (ctx) => {
140
154
}
141
155
} ) ;
142
156
157
+ router . post (
158
+ "/update-station-months/:stationId" ,
159
+ authMiddleware ,
160
+ async ( ctx ) => {
161
+ const stationId = Number ( ctx . params . stationId ) ;
162
+ const stationStats = await kv . get < StationStats > ( [ "station" , stationId ] ) ;
163
+ const tableDataEntries = await kv . list < TableDataItem > ( {
164
+ prefix : [ "windHistoryData" , stationId ] ,
165
+ } ) ;
166
+
167
+ for await ( const entry of tableDataEntries ) {
168
+ const date = parseObsTimeLocal ( entry . value . obsTimeLocal ) ;
169
+ const year = date . getFullYear ( ) ;
170
+ const month = date . getMonth ( ) ;
171
+
172
+ if (
173
+ ! stationStats . value ?. months . some (
174
+ ( m ) => m . year === year && m . month === month
175
+ )
176
+ ) {
177
+ stationStats . value ?. months . push ( { year, month } ) ;
178
+ }
179
+ }
180
+ await kv . set ( [ "station" , stationId ] , stationStats . value ) ;
181
+
182
+ ctx . response . status = 200 ;
183
+ }
184
+ ) ;
185
+
143
186
router . get ( "/stations" , async ( ctx ) => {
144
187
const stationsEntries = await kv . list < StationStats > ( {
145
188
prefix : [ "station" ] ,
@@ -152,20 +195,35 @@ router.get("/stations", async (ctx) => {
152
195
ctx . response . body = stations ;
153
196
} ) ;
154
197
155
- router . get ( "/wind-history/:stationId/csv " , async ( ctx ) => {
198
+ router . get ( "/wind-history/:stationId" , async ( ctx ) => {
156
199
const stationId = Number ( ctx . params . stationId ) ;
200
+ const fileformat = ctx . request . url . searchParams . get ( "fileformat" ) || "csv" ;
201
+ const year = Number ( ctx . request . url . searchParams . get ( "year" ) ) ;
202
+ const month = Number ( ctx . request . url . searchParams . get ( "month" ) ) ;
203
+ let datePrefix = [ ] ;
204
+ if ( year ) {
205
+ datePrefix . push ( year ) ;
206
+ if ( month ) datePrefix . push ( month ) ;
207
+ }
208
+
209
+ const filenameParts = [ "wind-history" , stationId . toString ( ) ] ;
210
+ if ( year ) {
211
+ filenameParts . push ( year . toString ( ) ) ;
212
+ if ( month ) {
213
+ filenameParts . push ( month . toString ( ) ) ;
214
+ }
215
+ }
216
+ const filename = filenameParts . join ( "-" ) + ".csv" ;
217
+
157
218
const entries = await kv . list < TableDataItem > ( {
158
- prefix : [ "windHistoryData" , stationId ] ,
219
+ prefix : [ "windHistoryData" , stationId , ... datePrefix ] ,
159
220
} ) ;
160
- const data : TableDataItem [ ] = [ ] ;
161
- for await ( const entry of entries ) {
162
- data . push ( entry . value ) ;
163
- }
164
- const csv = await stringify (
165
- data as unknown as readonly Record < string , unknown > [ ] ,
166
- {
167
- headers : true ,
168
- columns : [
221
+
222
+ const encoder = new TextEncoder ( ) ;
223
+ const stream = new ReadableStream ( {
224
+ async start ( controller ) {
225
+ // Write CSV headers
226
+ const headers = [
169
227
"id" ,
170
228
"stationId" ,
171
229
"obsTimeLocal" ,
@@ -178,16 +236,38 @@ router.get("/wind-history/:stationId/csv", async (ctx) => {
178
236
"humidityAvg" ,
179
237
"tempAvg" ,
180
238
"pressureAvg" ,
181
- ] ,
182
- }
183
- ) ;
239
+ ] ;
240
+ controller . enqueue ( encoder . encode ( headers . join ( "," ) + "\n" ) ) ;
241
+
242
+ // Write CSV data rows
243
+ for await ( const entry of entries ) {
244
+ const row = [
245
+ entry . value . id ,
246
+ entry . value . stationId ,
247
+ entry . value . obsTimeLocal ,
248
+ entry . value . winddirHigh ,
249
+ entry . value . winddirLow ,
250
+ entry . value . winddirAvg ,
251
+ entry . value . windspeedHigh ,
252
+ entry . value . windspeedAvg ,
253
+ entry . value . windspeedLow ,
254
+ entry . value . humidityAvg ,
255
+ entry . value . tempAvg ,
256
+ entry . value . pressureAvg ,
257
+ ] ;
258
+ controller . enqueue ( encoder . encode ( row . join ( "," ) + "\n" ) ) ;
259
+ }
260
+
261
+ controller . close ( ) ;
262
+ } ,
263
+ } ) ;
264
+
184
265
ctx . response . headers . set ( "Content-Type" , "text/csv" ) ;
185
- const date = new Date ( ) . toISOString ( ) . split ( "T" ) [ 0 ] ;
186
266
ctx . response . headers . set (
187
267
"Content-Disposition" ,
188
- `attachment; filename="wind-history- ${ stationId } - ${ date } .csv "`
268
+ `attachment; filename="${ filename } "`
189
269
) ;
190
- ctx . response . body = csv ;
270
+ ctx . response . body = stream ;
191
271
} ) ;
192
272
193
273
const app = new Application ( ) ;
0 commit comments